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 @@
1we are migrating to better-result for error handling.
2
3the 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
1121. `Phase 1` (RPC boundary first)
1132. `Phase 2` (tool discovery path split)
1143. `Phase 3` (OpenAPI split)
1154. `Phase 4` (type boundary cleanup)
1165. `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
243 any243 any
244 >;244 >;
245 };245 };
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 };
246 runtimeCallbacks: {386 runtimeCallbacks: {
247 completeRun: FunctionReference<387 completeRun: FunctionReference<
248 "mutation",388 "mutation",
@@ -1219,6 +1359,18 @@ export declare const internal: { · 970 unmodified lines
1219 any1359 any
1220 >;1360 >;
1221 };1361 };
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 };
1222 toolRegistry: {1374 toolRegistry: {
1223 beginBuild: FunctionReference<1375 beginBuild: FunctionReference<
1224 "mutation",1376 "mutation",
executor/packages/convex/workspace.ts+1−243
@@ -1,243 +1 @@
1import { v } from "convex/values";1export * from "./rpcs/public/workspace";
2import { internal } from "./_generated/api";
3import { mutation, query } from "./_generated/server";
4import { workspaceMutation, workspaceQuery } from "../core/src/function-builders";
5import { isAnonymousIdentity } from "./auth/anonymous";
6import { safeRunAfter } from "./lib/scheduler";
7
8const policyDecisionValidator = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny"));
9const credentialScopeValidator = v.union(v.literal("workspace"), v.literal("actor"));
10const credentialProviderValidator = v.union(v.literal("local-convex"), v.literal("workos-vault"));
11const toolSourceTypeValidator = v.union(v.literal("mcp"), v.literal("openapi"), v.literal("graphql"));
12
13function redactCredential<T extends { secretJson: Record<string, unknown> }>(credential: T): T {
14 return {
15 ...credential,
16 secretJson: {},
17 };
18}
19
20export 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
53export const listRuntimeTargets = query({
54 args: {},
55 handler: async (ctx) => {
56 return await ctx.runQuery(internal.database.listRuntimeTargets, {});
57 },
58});
59
60export 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
70export 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
79export 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
88export 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
105export 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
123export 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
132export 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
151export 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
162export const listCredentialProviders = query({
163 args: {},
164 handler: async (ctx) => {
165 return await ctx.runQuery(internal.database.listCredentialProviders, {});
166 },
167});
168
169export 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
188export 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
215export 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
224export 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});
executor/packages/core/src/tool/source-execution.ts+86−16
@@ -1,5 +1,19 @@
1import { Result, TaggedError } from "better-result";
1import { asRecord } from "../utils";2import { asRecord } from "../utils";
2 3
4type OpenApiRequestErrorArgs = {
5 status: number | null;
6 message: string;
7};
8
9type GraphqlRequestErrorArgs = {
10 message: string;
11};
12
13class OpenApiRequestError extends TaggedError("OpenApiRequestError")<OpenApiRequestErrorArgs>() {}
14
15class GraphqlRequestError extends TaggedError("GraphqlRequestError")<GraphqlRequestErrorArgs>() {}
16
3export interface OpenApiRequestRunSpec {17export interface OpenApiRequestRunSpec {
4 baseUrl: string;18 baseUrl: string;
5 method: string;19 method: string;
@@ -73,7 +87,7 @@ export async function executeOpenApiRequest( · 67 unmodified lines
73 runSpec: OpenApiRequestRunSpec,87 runSpec: OpenApiRequestRunSpec,
74 input: unknown,88 input: unknown,
75 credentialHeaders?: Record<string, string>,89 credentialHeaders?: Record<string, string>,
76): Promise<unknown> {90): Promise<Result<unknown, OpenApiRequestError>> {
77 const payload = asRecord(input);91 const payload = asRecord(input);
78 const readMethods = new Set(["get", "head", "options"]);92 const readMethods = new Set(["get", "head", "options"]);
79 const { url, bodyInput } = buildOpenApiUrl(93 const { url, bodyInput } = buildOpenApiUrl(
@@ -84,7 +98,7 @@ export async function executeOpenApiRequest( · 4 unmodified lines
84 );98 );
85 const hasBody = !readMethods.has(runSpec.method) && Object.keys(bodyInput).length > 0;99 const hasBody = !readMethods.has(runSpec.method) && Object.keys(bodyInput).length > 0;
86 100
87 const response = await fetch(url, {101 const request = Result.try(() => ({
88 method: runSpec.method.toUpperCase(),102 method: runSpec.method.toUpperCase(),
89 headers: {103 headers: {
90 ...runSpec.authHeaders,104 ...runSpec.authHeaders,
@@ -92,18 +106,50 @@ export async function executeOpenApiRequest( · 1 unmodified line
92 ...(hasBody ? { "content-type": "application/json" } : {}),106 ...(hasBody ? { "content-type": "application/json" } : {}),
93 },107 },
94 body: hasBody ? JSON.stringify(bodyInput) : undefined,108 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;
96 127
97 if (!response.ok) {128 if (!response.ok) {
98 const text = await response.text().catch(() => "");129 const text = await Result.tryPromise(() => response.text())
99 throw new Error(`HTTP ${response.status} ${response.statusText}: ${text.slice(0, 500)}`);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 }));
100 }136 }
101 137
102 const contentType = response.headers.get("content-type") ?? "";138 const contentType = response.headers.get("content-type") ?? "";
103 if (contentType.includes("json")) {139 const bodyResult = contentType.includes("json")
104 return await response.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 }));
105 }150 }
106 return await response.text();151
152 return Result.ok(bodyResult.value);
107}153}
108 154
109function hasGraphqlData(data: unknown): boolean {155function hasGraphqlData(data: unknown): boolean {
@@ -126,8 +172,8 @@ export async function executeGraphqlRequest( · 16 unmodified lines
126 query: string,172 query: string,
127 variables: unknown,173 variables: unknown,
128 credentialHeaders?: Record<string, string>,174 credentialHeaders?: Record<string, string>,
129): Promise<GraphqlExecutionEnvelope> {175): Promise<Result<GraphqlExecutionEnvelope, GraphqlRequestError>> {
130 const response = await fetch(endpoint, {176 const responseResult = await Result.tryPromise(async () => fetch(endpoint, {
131 method: "POST",177 method: "POST",
132 headers: {178 headers: {
133 "content-type": "application/json",179 "content-type": "application/json",
@@ -135,17 +181,41 @@ export async function executeGraphqlRequest( · 1 unmodified line
135 ...(credentialHeaders ?? {}),181 ...(credentialHeaders ?? {}),
136 },182 },
137 body: JSON.stringify({ query, variables }),183 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;
139 194
140 if (!response.ok) {195 if (!response.ok) {
141 const text = await response.text().catch(() => "");196 const text = await Result.tryPromise(() => response.text()).unwrapOr("");
142 throw new Error(`HTTP ${response.status} ${response.statusText}: ${text.slice(0, 500)}`);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 }));
143 }211 }
144 212
145 const result = await response.json() as { data?: unknown; errors?: unknown[] };213 const decoded = result.value;
146 if (result.errors && !hasGraphqlData(result.data)) {214 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 }));
148 }218 }
149 219
150 return normalizeGraphqlEnvelope(result);220 return Result.ok(normalizeGraphqlEnvelope(decoded));
151}221}
opencode.json+1−1
@@ -13,9 +13,9 @@ · 12 unmodified lines
13 }13 }
14 },14 },
15 "agent":{15 "agent":{
16
17 "quick-build": {16 "quick-build": {
18 "mode": "primary",17 "mode": "primary",
18 "description": "A smaller context winder but extremely fast at working",
19 "model": "openai/gpt-5.3-codex-spark",19 "model": "openai/gpt-5.3-codex-spark",
20 "tools": {20 "tools": {
21 "write": true,21 "write": true,
ralph.ts+20−0
@@ -0,0 +1,20 @@
1import { $ } from "bun"
2import { readFileSync } from "node:fs"
3const coreRefactorGoal = readFileSync("executor/convex-core-refactor-code-plan", "utf-8")
4
5const STOP_TOKEN = '<I HAVE COMPLETED THE TASK>'
6
7const 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
10async 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
19await run(coreRefactorGoal)
20