History

ralphin it

Rhys Sullivan committed 8 months ago

Commit 898c5641 parent 1d2cc797 files · +476 −260Browse files

Unified · Split

7 files
  1. executor/better-result-migration.md +92 −0
  2. executor/convex-core-refactor-code-plan.md +124 −0
  3. executor/packages/convex/_generated/api.d.ts +152 −0
  4. executor/packages/convex/workspace.ts +1 −243
  5. executor/packages/core/src/tool/source-execution.ts +86 −16
  6. opencode.json +1 −1
  7. ralph.ts +20 −0
executor/better-result-migration.md+92−0
@@ -0,0 +1,92 @@
1+we are migrating to better-result for error handling.
2+
3+the docs are avaliable in the readme of https://github.com/dmmulroy/better-result
4+
5+- Goal: Move executor/core/convex error flow to better-result for recoverable failures, while keeping cleanup/irrecoverable defects as try/catch.
6+- Success criterion: Same behavior, clearer typed failures, and easier composition via Result and TaggedError.
7+- Constraint: No behavior regressions; keep existing fallbacks and best-effort paths intact.
8+ Phase 0 — Baseline and Contract
9+- Map current error behavior before edits:
10+ - executor: capture current try/catch counts and key failure paths.
11+ - Identify true “domain errors” vs. “defect exceptions” in each file.
12+- Define a shared error taxonomy for each boundary:
13+ - parse/normalization, external I/O, auth, tool execution, cache/db, scheduler/runtime, and JSON/network parsing.
14+- Add/confirm:
15+ - standard ErrorShape (TaggedError classes),
16+ - resultErrorMessage, unwrapError, and one boundary mapper for HTTP/route/Convex outputs.
17+- Approval gate: every new Result path must carry a typed error model (Result<T, AppErrorUnion>), not unknown.
18+ Phase 1 — Core Runtime Utilities (lowest blast radius first)
19+- Convert deterministic/small helpers first:
20+ - executor/packages/core/src/type-format.ts
21+ - executor/packages/core/src/runtimes/transpile.ts
22+ - executor/packages/core/src/runtimes/runtime-core.ts internal helpers like serialization.
23+- Add/adjust:
24+ - Result.try/Result.tryPromise,
25+ - Result.map/Result.mapError for error normalization,
26+ - catch blocks removed where they only wrap parse/IO failures into domain errors.
27+- Gate:
28+ - function signatures updated to return Result,
29+ - no semantic behavior change on success path,
30+ - unit tests updated/extended where error normalization changed.
31+ Phase 2 — Core Feature Pipelines
32+- Convert multi-step async flows with generator style where it improves clarity:
33+ - executor/packages/core/src/openapi-prepare.ts
34+ - executor/packages/core/src/tool/source-execution.ts
35+ - executor/packages/core/src/credential-providers.ts, tool_source_loaders/\*, other API prep/loaders.
36+- Refactor rule:
37+ - try only used when:
38+ - wrapping non-domain callback boundaries,
39+ - doing cleanup side effects,
40+ - preserving compatibility with external SDKs that throw.
41+- Gate:
42+ - each pipeline uses Result.gen/Result.await or explicit isErr checks for early return,
43+ - tagged errors are normalized at the pipeline boundary.
44+ Phase 3 — Convex Runtime/Auth/Tool Orchestration
45+- Convert orchestration-heavy files carefully, file-by-file:
46+ - executor/packages/convex/runtime/workspace_tools.ts
47+ - executor/packages/convex/runtime/tool_source_loading.ts
48+ - executor/packages/convex/runtime/tool_invocation.ts
49+ - MCP auth handler path in executor/packages/convex/http/mcp_handler.ts
50+- Do this in place with minimal contract changes:
51+ - preserve DB/auth semantics,
52+ - only change internal control flow to return Result until final response is built.
53+- Gate:
54+ - access-control and workspace behavior unchanged,
55+ - tests in access-controls.test.ts and related e2e suites still pass,
56+ - no untyped catch returning silent partial failures except cleanup.
57+ Phase 4 — Frontend Routes and Call Sites
58+- Finish existing edge conversions where already partial:
59+ - executor/apps/web/src/app/mcp/oauth/start/route.ts
60+ - executor/apps/web/src/app/mcp/oauth/callback/route.ts
61+ - executor/apps/web/src/app/mcp/oauth/detect/route.ts
62+ - executor/apps/web/src/lib/mcp/oauth-provider.ts
63+ - any remaining Result-ready route handlers.
64+- Standardize response shaping:
65+ - always keep user-facing messages stable,
66+ - map non-domain panics separately from expected recoverable failures.
67+- Gate:
68+ - UI flow unaffected,
69+ - no new thrown errors for expected validation problems.
70+ Phase 5 — Assistant + Sources Sweep
71+- After executor and convex are stable, apply selective clean-up:
72+ - assistant/packages/core/src/agent.ts
73+ - assistant/packages/bot/src/index.ts
74+ - sources modules where applicable.
75+- Gate:
76+ - only low-risk, high-signal conversions,
77+ - no broad style churn.
78+ Validation and “complete” criteria
79+- Static checks:
80+ - bun run typecheck
81+ - bun run test
82+ - bun run test:executor
83+ - bun run test:assistant
84+ - bun run knip
85+- Semantic checks:
86+ - All converted paths preserve success output and existing logging/metrics signals.
87+ - panic use remains only for invariant/defect cases, not domain failures.
88+ - Error payloads at external boundaries remain JSON-serializable and actionable.
89+- Completion gate:
90+ - No panic introduced where a recoverable Err was expected,
91+ - No new behavior regressions in OAuth/tooling execution/authz flows,
92+ - Migration notes in a tracking doc include every touched file + acceptance result (pass/fail).
executor/convex-core-refactor-code-plan.md+124−0
@@ -0,0 +1,124 @@
1+# Convex/Core Greenfield Refactor Plan (Code-Only)
2+
3+## Scope
4+- Keep all Convex schema ownership in `executor/packages/convex/schema.ts`.
5+- Treat `executor/packages/core` as the home for pure transformation/domain logic.
6+- Keep `executor/packages/convex` as orchestration boundary: DB writes/reads, authz, scheduler, side effects.
7+- Do not add script/docs-only changes in this pass; only code changes are planned.
8+
9+## Phase 1 — RPC Boundary (read/write split only)
10+
11+### 1.1 Add RPC barrel directories
12+- Add:
13+ - `executor/packages/convex/rpcs/public/index.ts`
14+ - `executor/packages/convex/rpcs/internal/index.ts`
15+- Add optional domain files under each folder (no behavior logic).
16+
17+### 1.2 Move public RPC declarations into `rpcs/public`
18+- Move every public `query`, `mutation`, `action` currently declared outside `rpcs/public` into the public barrel files.
19+- Start with these likely sources:
20+ - `executor/packages/convex/workspace.ts`
21+ - `executor/packages/convex/app.ts`
22+ - `executor/packages/convex/executor.ts`
23+ - `executor/packages/convex/executorNode.ts`
24+ - `executor/packages/convex/runtime/*`
25+ - `executor/packages/convex/task/*`
26+ - `executor/packages/convex/database.ts`
27+- Keep only pure functions/helpers in origin files and export/import through public barrel.
28+
29+### 1.3 Move internal RPC declarations into `rpcs/internal`
30+- Move all `internalMutation`, `internalAction`, `internalQuery` declarations to:
31+ - `executor/packages/convex/rpcs/internal/*`
32+- Keep orchestration logic in helper files and call them from these declarations.
33+
34+### 1.4 Thin Convex entrypoints
35+- Refactor:
36+ - `executor/packages/convex/http.ts`
37+ - `executor/packages/convex/app.ts`
38+ - `executor/packages/convex/workspace.ts`
39+- Convert these to wiring/re-export modules for the RPC barrels.
40+
41+## Phase 2 — Extraction: tool path/discovery pure functions
42+
43+### 2.1 Move tool path utilities
44+- Move pure path utilities from:
45+ - `executor/packages/convex/runtime/tool_paths.ts`
46+- To:
47+ - `executor/packages/core/src/tool-discovery/tool-paths.ts`
48+
49+### 2.2 Split `workspace_tools.ts`
50+- In `executor/packages/convex/runtime/workspace_tools.ts` separate:
51+ - Pure parsing/normalization/formatting functions -> `executor/packages/core/src/tool-discovery/workspace-tools.ts`
52+ - DB/credential/scheduler/cache-dependent functions -> remain in Convex.
53+
54+### 2.3 Move pure call-resolution helpers
55+- Move deterministic formatting/resolution helpers from:
56+ - `executor/packages/convex/runtime/tool_call_resolution.ts`
57+- To:
58+ - `executor/packages/core/src/tool-discovery/tool-call-resolution.ts`
59+- Keep only stateful invocation logic in Convex runtime.
60+
61+## Phase 3 — OpenAPI pipeline split
62+
63+### 3.1 Keep pure OpenAPI transforms in core
64+- Ensure and consolidate pure modules under core:
65+ - `executor/packages/core/src/openapi-prepare.ts`
66+ - `executor/packages/core/src/openapi/tool-builder.ts`
67+ - `executor/packages/core/src/openapi/schema-hints.ts`
68+
69+### 3.2 Keep side-effect pipeline in Convex
70+- Keep/adjust Convex-side orchestration + storage in:
71+ - `executor/packages/convex/runtime/tool_source_loading.ts`
72+ - related source-loading callers in `executor/packages/convex/runtime/*`
73+- Refactor callers so Convex passes DB/context to pure core functions, then persists outputs.
74+
75+## Phase 4 — Core type boundary cleanup
76+
77+### 4.1 Remove Convex type leakage from core modules
78+- Refactor:
79+ - `executor/packages/core/src/function-builders.ts`
80+ - `executor/packages/core/src/identity.ts`
81+ - `executor/packages/core/src/types.ts`
82+- Replace direct Convex runtime types (`_generated` docs/ids where possible) with domain contracts.
83+
84+### 4.2 Add boundary adapters in Convex
85+- Add/expand:
86+ - `executor/packages/convex/adapters/contracts.ts`
87+- Move/centralize conversion helpers:
88+ - Convex `Id` <-> domain IDs
89+ - Convex `Doc`-shaped runtime records <-> Core DTOs
90+
91+### 4.3 Update call sites to use adapters
92+- Update all Convex <-> Core calls to pass/receive adapter-shaped values.
93+- Ensure no core file imports Convex internal generated modules for general logic.
94+
95+## Phase 5 — Final extraction sweep and cleanup
96+
97+### 5.1 Move additional pure utilities discovered in `runtime`/`task`
98+- Audit:
99+ - `executor/packages/convex/runtime/*`
100+ - `executor/packages/convex/task/*`
101+- For each pure function file, move to `executor/packages/core/src/*` and keep only orchestration-side effects in Convex.
102+
103+### 5.2 Normalize imports after each move
104+- Replace moved imports to use core modules.
105+- Keep any DB, auth, scheduler, or external-call side effects behind Convex modules.
106+
107+### 5.3 Delete empty/obsolete runtime modules
108+- Remove Convex files reduced to no logic after extraction.
109+- Keep small proxy wrappers only when needed to avoid breaking imports.
110+
111+## Mandatory implementation order
112+1. `Phase 1` (RPC boundary first)
113+2. `Phase 2` (tool discovery path split)
114+3. `Phase 3` (OpenAPI split)
115+4. `Phase 4` (type boundary cleanup)
116+5. `Phase 5` (final sweep)
117+
118+## Concrete acceptance criteria (code-level)
119+- All RPC declarations are in `executor/packages/convex/rpcs/public/*` or `executor/packages/convex/rpcs/internal/*`.
120+- `executor/packages/convex/runtime/tool_paths.ts` is either removed or only contains Convex-specific wrappers.
121+- `executor/packages/convex/runtime/workspace_tools.ts` has only stateful/runtime logic.
122+- OpenAPI parsing/type transformation logic has no Convex DB/auth/scheduler side effects.
123+- Core packages can typecheck without `executor/packages/convex/_generated/*` imports for non-boundary modules.
124+- Any Convex/Core boundary exchange goes through adapter conversion points.
executor/packages/convex/_generated/api.d.ts+152−0
@@ -243,6 +243,146 @@ export declare const api: { · 242 unmodified lines
243243 any
244244 >;
245245 };
246+ rpcs: {
247+ public: {
248+ app: {
249+ getClientConfig: FunctionReference<"query", "public", {}, any>;
250+ getCurrentAccount: FunctionReference<
251+ "query",
252+ "public",
253+ { sessionId?: string },
254+ any
255+ >;
256+ };
257+ auth: {
258+ bootstrapCurrentWorkosAccount: FunctionReference<
259+ "mutation",
260+ "public",
261+ {},
262+ any
263+ >;
264+ };
265+ workspace: {
266+ bootstrapAnonymousSession: FunctionReference<
267+ "mutation",
268+ "public",
269+ { actorId?: string; sessionId?: string },
270+ any
271+ >;
272+ deleteToolSource: FunctionReference<
273+ "mutation",
274+ "public",
275+ {
276+ sessionId?: string;
277+ sourceId: string;
278+ workspaceId: Id<"workspaces">;
279+ },
280+ any
281+ >;
282+ getTaskInWorkspace: FunctionReference<
283+ "query",
284+ "public",
285+ { sessionId?: string; taskId: string; workspaceId: Id<"workspaces"> },
286+ any
287+ >;
288+ listAccessPolicies: FunctionReference<
289+ "query",
290+ "public",
291+ { sessionId?: string; workspaceId: Id<"workspaces"> },
292+ any
293+ >;
294+ listCredentialProviders: FunctionReference<"query", "public", {}, any>;
295+ listCredentials: FunctionReference<
296+ "query",
297+ "public",
298+ { sessionId?: string; workspaceId: Id<"workspaces"> },
299+ any
300+ >;
301+ listPendingApprovals: FunctionReference<
302+ "query",
303+ "public",
304+ { sessionId?: string; workspaceId: Id<"workspaces"> },
305+ any
306+ >;
307+ listRuntimeTargets: FunctionReference<"query", "public", {}, any>;
308+ listTaskEvents: FunctionReference<
309+ "query",
310+ "public",
311+ { sessionId?: string; taskId: string; workspaceId: Id<"workspaces"> },
312+ any
313+ >;
314+ listTasks: FunctionReference<
315+ "query",
316+ "public",
317+ { sessionId?: string; workspaceId: Id<"workspaces"> },
318+ any
319+ >;
320+ listToolSources: FunctionReference<
321+ "query",
322+ "public",
323+ { sessionId?: string; workspaceId: Id<"workspaces"> },
324+ any
325+ >;
326+ resolveCredential: FunctionReference<
327+ "query",
328+ "public",
329+ {
330+ actorId?: string;
331+ scope: "workspace" | "actor";
332+ sessionId?: string;
333+ sourceKey: string;
334+ workspaceId: Id<"workspaces">;
335+ },
336+ any
337+ >;
338+ upsertAccessPolicy: FunctionReference<
339+ "mutation",
340+ "public",
341+ {
342+ actorId?: string;
343+ clientId?: string;
344+ decision: "allow" | "require_approval" | "deny";
345+ id?: string;
346+ priority?: number;
347+ sessionId?: string;
348+ toolPathPattern: string;
349+ workspaceId: Id<"workspaces">;
350+ },
351+ any
352+ >;
353+ upsertCredential: FunctionReference<
354+ "mutation",
355+ "public",
356+ {
357+ actorId?: string;
358+ id?: string;
359+ overridesJson?: any;
360+ provider?: "local-convex" | "workos-vault";
361+ scope: "workspace" | "actor";
362+ secretJson: any;
363+ sessionId?: string;
364+ sourceKey: string;
365+ workspaceId: Id<"workspaces">;
366+ },
367+ any
368+ >;
369+ upsertToolSource: FunctionReference<
370+ "mutation",
371+ "public",
372+ {
373+ config: any;
374+ enabled?: boolean;
375+ id?: string;
376+ name: string;
377+ sessionId?: string;
378+ type: "mcp" | "openapi" | "graphql";
379+ workspaceId: Id<"workspaces">;
380+ },
381+ any
382+ >;
383+ };
384+ };
385+ };
246386 runtimeCallbacks: {
247387 completeRun: FunctionReference<
248388 "mutation",
@@ -1219,6 +1359,18 @@ export declare const internal: { · 970 unmodified lines
12191359 any
12201360 >;
12211361 };
1362+ rpcs: {
1363+ public: {
1364+ auth: {
1365+ authKitEvent: FunctionReference<
1366+ "mutation",
1367+ "internal",
1368+ { data: Record<string, any>; event: string },
1369+ null
1370+ >;
1371+ };
1372+ };
1373+ };
12221374 toolRegistry: {
12231375 beginBuild: FunctionReference<
12241376 "mutation",
executor/packages/convex/workspace.ts+1−243
@@ -1,243 +1 @@
1−import { v } from "convex/values";
2−import { internal } from "./_generated/api";
3−import { mutation, query } from "./_generated/server";
4−import { workspaceMutation, workspaceQuery } from "../core/src/function-builders";
5−import { isAnonymousIdentity } from "./auth/anonymous";
6−import { safeRunAfter } from "./lib/scheduler";
7−
8−const policyDecisionValidator = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny"));
9−const credentialScopeValidator = v.union(v.literal("workspace"), v.literal("actor"));
10−const credentialProviderValidator = v.union(v.literal("local-convex"), v.literal("workos-vault"));
11−const toolSourceTypeValidator = v.union(v.literal("mcp"), v.literal("openapi"), v.literal("graphql"));
12−
13−function redactCredential<T extends { secretJson: Record<string, unknown> }>(credential: T): T {
14− return {
15− ...credential,
16− secretJson: {},
17− };
18−}
19−
20−export const bootstrapAnonymousSession = mutation({
21− args: {
22− sessionId: v.optional(v.string()),
23− actorId: v.optional(v.string()),
24− },
25− handler: async (ctx, args) => {
26− const identity = await ctx.auth.getUserIdentity().catch(() => null);
27− const requestedSessionId = args.sessionId?.trim() || undefined;
28− const requestedActorId = args.actorId?.trim() || undefined;
29−
30− if (identity && !isAnonymousIdentity(identity)) {
31− throw new Error("Cannot bootstrap an anonymous session while authenticated");
32− }
33−
34− const actorId = identity ? identity.subject : requestedActorId;
35− if (actorId && !actorId.startsWith("anon_")) {
36− throw new Error("actorId must be an anonymous actor (anon_*)");
37− }
38−
39− // Allow unauthenticated callers to bootstrap a fresh anonymous session.
40− // The internal mutation will generate ids when not provided.
41− const sessionId = identity
42− ? (requestedSessionId ?? `anon_session_${actorId}`)
43− : requestedSessionId;
44−
45− return await ctx.runMutation(internal.database.bootstrapAnonymousSession, {
46− ...(sessionId ? { sessionId } : {}),
47− ...(actorId ? { actorId } : {}),
48− clientId: "web",
49− });
50− },
51−});
52−
53−export const listRuntimeTargets = query({
54− args: {},
55− handler: async (ctx) => {
56− return await ctx.runQuery(internal.database.listRuntimeTargets, {});
57− },
58−});
59−
60−export const getTaskInWorkspace = workspaceQuery({
61− args: { taskId: v.string() },
62− handler: async (ctx, args) => {
63− return await ctx.runQuery(internal.database.getTaskInWorkspace, {
64− taskId: args.taskId,
65− workspaceId: ctx.workspaceId,
66− });
67− },
68−});
69−
70−export const listTasks = workspaceQuery({
71− args: {},
72− handler: async (ctx) => {
73− return await ctx.runQuery(internal.database.listTasks, {
74− workspaceId: ctx.workspaceId,
75− });
76− },
77−});
78−
79−export const listPendingApprovals = workspaceQuery({
80− args: {},
81− handler: async (ctx) => {
82− return await ctx.runQuery(internal.database.listPendingApprovals, {
83− workspaceId: ctx.workspaceId,
84− });
85− },
86−});
87−
88−export const listTaskEvents = workspaceQuery({
89− args: { taskId: v.string() },
90− handler: async (ctx, args) => {
91− const task = await ctx.runQuery(internal.database.getTaskInWorkspace, {
92− taskId: args.taskId,
93− workspaceId: ctx.workspaceId,
94− });
95− if (!task) {
96− return [];
97− }
98−
99− return await ctx.runQuery(internal.database.listTaskEvents, {
100− taskId: args.taskId,
101− });
102− },
103−});
104−
105−export const upsertAccessPolicy = workspaceMutation({
106− requireAdmin: true,
107− args: {
108− id: v.optional(v.string()),
109− actorId: v.optional(v.string()),
110− clientId: v.optional(v.string()),
111− toolPathPattern: v.string(),
112− decision: policyDecisionValidator,
113− priority: v.optional(v.number()),
114− },
115− handler: async (ctx, args) => {
116− return await ctx.runMutation(internal.database.upsertAccessPolicy, {
117− workspaceId: ctx.workspaceId,
118− ...args,
119− });
120− },
121−});
122−
123−export const listAccessPolicies = workspaceQuery({
124− args: {},
125− handler: async (ctx) => {
126− return await ctx.runQuery(internal.database.listAccessPolicies, {
127− workspaceId: ctx.workspaceId,
128− });
129− },
130−});
131−
132−export const upsertCredential = workspaceMutation({
133− requireAdmin: true,
134− args: {
135− id: v.optional(v.string()),
136− sourceKey: v.string(),
137− scope: credentialScopeValidator,
138− actorId: v.optional(v.string()),
139− provider: v.optional(credentialProviderValidator),
140− secretJson: v.any(),
141− overridesJson: v.optional(v.any()),
142− },
143− handler: async (ctx, args) => {
144− return await ctx.runMutation(internal.database.upsertCredential, {
145− workspaceId: ctx.workspaceId,
146− ...args,
147− });
148− },
149−});
150−
151−export const listCredentials = workspaceQuery({
152− args: {},
153− handler: async (ctx) => {
154− const credentials = await ctx.runQuery(internal.database.listCredentials, {
155− workspaceId: ctx.workspaceId,
156− });
157−
158− return credentials.map(redactCredential);
159− },
160−});
161−
162−export const listCredentialProviders = query({
163− args: {},
164− handler: async (ctx) => {
165− return await ctx.runQuery(internal.database.listCredentialProviders, {});
166− },
167−});
168−
169−export const resolveCredential = workspaceQuery({
170− requireAdmin: true,
171− args: {
172− sourceKey: v.string(),
173− scope: credentialScopeValidator,
174− actorId: v.optional(v.string()),
175− },
176− handler: async (ctx, args) => {
177− const credential = await ctx.runQuery(internal.database.resolveCredential, {
178− workspaceId: ctx.workspaceId,
179− sourceKey: args.sourceKey,
180− scope: args.scope,
181− actorId: args.actorId,
182− });
183−
184− return credential ? redactCredential(credential) : null;
185− },
186−});
187−
188−export const upsertToolSource = workspaceMutation({
189− requireAdmin: true,
190− args: {
191− id: v.optional(v.string()),
192− name: v.string(),
193− type: toolSourceTypeValidator,
194− config: v.any(),
195− enabled: v.optional(v.boolean()),
196− },
197− handler: async (ctx, args) => {
198− const source = await ctx.runMutation(internal.database.upsertToolSource, {
199− workspaceId: ctx.workspaceId,
200− ...args,
201− });
202−
203− try {
204− await safeRunAfter(ctx.scheduler, 0, internal.executorNode.listToolsWithWarningsInternal, {
205− workspaceId: ctx.workspaceId,
206− });
207− } catch {
208− // Best effort prewarm only.
209− }
210−
211− return source;
212− },
213−});
214−
215−export const listToolSources = workspaceQuery({
216− args: {},
217− handler: async (ctx) => {
218− return await ctx.runQuery(internal.database.listToolSources, {
219− workspaceId: ctx.workspaceId,
220− });
221− },
222−});
223−
224−export const deleteToolSource = workspaceMutation({
225− requireAdmin: true,
226− args: { sourceId: v.string() },
227− handler: async (ctx, args) => {
228− const deleted = await ctx.runMutation(internal.database.deleteToolSource, {
229− workspaceId: ctx.workspaceId,
230− sourceId: args.sourceId,
231− });
232−
233− try {
234− await safeRunAfter(ctx.scheduler, 0, internal.executorNode.listToolsWithWarningsInternal, {
235− workspaceId: ctx.workspaceId,
236− });
237− } catch {
238− // Best effort prewarm only.
239− }
240−
241− return deleted;
242− },
243−});
1+export * from "./rpcs/public/workspace";
executor/packages/core/src/tool/source-execution.ts+86−16
@@ -1,5 +1,19 @@
1+import { Result, TaggedError } from "better-result";
12 import { asRecord } from "../utils";
23
4+type OpenApiRequestErrorArgs = {
5+ status: number | null;
6+ message: string;
7+};
8+
9+type GraphqlRequestErrorArgs = {
10+ message: string;
11+};
12+
13+class OpenApiRequestError extends TaggedError("OpenApiRequestError")<OpenApiRequestErrorArgs>() {}
14+
15+class GraphqlRequestError extends TaggedError("GraphqlRequestError")<GraphqlRequestErrorArgs>() {}
16+
317 export interface OpenApiRequestRunSpec {
418 baseUrl: string;
519 method: string;
@@ -73,7 +87,7 @@ export async function executeOpenApiRequest( · 67 unmodified lines
7387 runSpec: OpenApiRequestRunSpec,
7488 input: unknown,
7589 credentialHeaders?: Record<string, string>,
76−): Promise<unknown> {
90+): Promise<Result<unknown, OpenApiRequestError>> {
7791 const payload = asRecord(input);
7892 const readMethods = new Set(["get", "head", "options"]);
7993 const { url, bodyInput } = buildOpenApiUrl(
@@ -84,7 +98,7 @@ export async function executeOpenApiRequest( · 4 unmodified lines
8498 );
8599 const hasBody = !readMethods.has(runSpec.method) && Object.keys(bodyInput).length > 0;
86100
87− const response = await fetch(url, {
101+ const request = Result.try(() => ({
88102 method: runSpec.method.toUpperCase(),
89103 headers: {
90104 ...runSpec.authHeaders,
@@ -92,18 +106,50 @@ export async function executeOpenApiRequest( · 1 unmodified line
92106 ...(hasBody ? { "content-type": "application/json" } : {}),
93107 },
94108 body: hasBody ? JSON.stringify(bodyInput) : undefined,
95− });
109+ }));
110+ if (request.isErr()) {
111+ return Result.err(new OpenApiRequestError({
112+ status: null,
113+ message: request.error.message,
114+ }));
115+ }
116+
117+ const responseResult = await Result.tryPromise(async () => fetch(url, request.value));
118+ if (responseResult.isErr()) {
119+ const cause = responseResult.error.cause;
120+ return Result.err(new OpenApiRequestError({
121+ status: null,
122+ message: `OpenAPI request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
123+ }));
124+ }
125+
126+ const response = responseResult.value;
96127
97128 if (!response.ok) {
98− const text = await response.text().catch(() => "");
99− throw new Error(`HTTP ${response.status} ${response.statusText}: ${text.slice(0, 500)}`);
129+ const text = await Result.tryPromise(() => response.text())
130+ .map((value) => value.slice(0, 500))
131+ .unwrapOr("");
132+ return Result.err(new OpenApiRequestError({
133+ status: response.status,
134+ message: `HTTP ${response.status} ${response.statusText}: ${text}`,
135+ }));
100136 }
101137
102138 const contentType = response.headers.get("content-type") ?? "";
103− if (contentType.includes("json")) {
104− return await response.json();
139+ const bodyResult = contentType.includes("json")
140+ ? await Result.tryPromise(async () => response.json() as Promise<unknown>)
141+ : await Result.tryPromise(() => response.text() as Promise<string>);
142+
143+ if (bodyResult.isErr()) {
144+ return Result.err(new OpenApiRequestError({
145+ status: response.status,
146+ message: `Failed to read OpenAPI response body: ${
147+ bodyResult.error instanceof Error ? bodyResult.error.message : String(bodyResult.error)
148+ }`,
149+ }));
105150 }
106− return await response.text();
151+
152+ return Result.ok(bodyResult.value);
107153 }
108154
109155 function hasGraphqlData(data: unknown): boolean {
@@ -126,8 +172,8 @@ export async function executeGraphqlRequest( · 16 unmodified lines
126172 query: string,
127173 variables: unknown,
128174 credentialHeaders?: Record<string, string>,
129−): Promise<GraphqlExecutionEnvelope> {
130− const response = await fetch(endpoint, {
175+): Promise<Result<GraphqlExecutionEnvelope, GraphqlRequestError>> {
176+ const responseResult = await Result.tryPromise(async () => fetch(endpoint, {
131177 method: "POST",
132178 headers: {
133179 "content-type": "application/json",
@@ -135,17 +181,41 @@ export async function executeGraphqlRequest( · 1 unmodified line
135181 ...(credentialHeaders ?? {}),
136182 },
137183 body: JSON.stringify({ query, variables }),
138− });
184+ }));
185+
186+ if (responseResult.isErr()) {
187+ const cause = responseResult.error.cause;
188+ return Result.err(new GraphqlRequestError({
189+ message: `GraphQL request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
190+ }));
191+ }
192+
193+ const response = responseResult.value;
139194
140195 if (!response.ok) {
141− const text = await response.text().catch(() => "");
142− throw new Error(`HTTP ${response.status} ${response.statusText}: ${text.slice(0, 500)}`);
196+ const text = await Result.tryPromise(() => response.text()).unwrapOr("");
197+ return Result.err(new GraphqlRequestError({
198+ message: `HTTP ${response.status} ${response.statusText}: ${text.slice(0, 500)}`,
199+ }));
200+ }
201+
202+ const result = await Result.tryPromise(async () =>
203+ response.json() as Promise<{ data?: unknown; errors?: unknown[] }>
204+ );
205+ if (result.isErr()) {
206+ return Result.err(new GraphqlRequestError({
207+ message: `Failed to parse GraphQL response: ${
208+ result.error instanceof Error ? result.error.message : String(result.error)
209+ }`,
210+ }));
143211 }
144212
145− const result = await response.json() as { data?: unknown; errors?: unknown[] };
213+ const decoded = result.value;
146214 if (result.errors && !hasGraphqlData(result.data)) {
147− throw new Error(`GraphQL errors: ${JSON.stringify(result.errors).slice(0, 1000)}`);
215+ return Result.err(new GraphqlRequestError({
216+ message: `GraphQL errors: ${JSON.stringify(result.errors).slice(0, 1000)}`,
217+ }));
148218 }
149219
150− return normalizeGraphqlEnvelope(result);
220+ return Result.ok(normalizeGraphqlEnvelope(decoded));
151221 }
opencode.json+1−1
@@ -13,9 +13,9 @@ · 12 unmodified lines
1313 }
1414 },
1515 "agent":{
16−
1716 "quick-build": {
1817 "mode": "primary",
18+ "description": "A smaller context winder but extremely fast at working",
1919 "model": "openai/gpt-5.3-codex-spark",
2020 "tools": {
2121 "write": true,
ralph.ts+20−0
@@ -0,0 +1,20 @@
1+import { $ } from "bun"
2+import { readFileSync } from "node:fs"
3+const coreRefactorGoal = readFileSync("executor/convex-core-refactor-code-plan", "utf-8")
4+
5+const STOP_TOKEN = '<I HAVE COMPLETED THE TASK>'
6+
7+const makePrompt = (goal: string) => `Continue working until you believe the task is complete. As a reminder, the goal is: ${goal}. The above goal was copy pasted in, resume from where you left off. Output ${STOP_TOKEN} when you have completed the task.`
8+
9+
10+async function run(goal: string) {
11+ const prompt = makePrompt(goal)
12+ let ralph = ''
13+ while (!ralph.includes(STOP_TOKEN)) {
14+ ralph = await $`opencode run --attach http://100.81.219.45:39821 --continue ${prompt}`.text()
15+ }
16+ return ralph
17+}
18+
19+await run(coreRefactorGoal)
20+