Unified · Split
| @@ -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). | ||
| @@ -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. | ||
| @@ -243,6 +243,146 @@ export declare const api: { · 242 unmodified lines | |||
| 243 | any | 243 | 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 | any | 1359 | 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", |
| @@ -1,243 +1 @@ | |||
| 1 | import { v } from "convex/values"; | 1 | export * from "./rpcs/public/workspace"; |
| 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,5 +1,19 @@ | |||
| 1 | import { Result, TaggedError } from "better-result"; | ||
| 1 | import { asRecord } from "../utils"; | 2 | import { asRecord } from "../utils"; |
| 2 | 3 | ||
| 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 | |||
| 3 | export interface OpenApiRequestRunSpec { | 17 | export 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 | ||
| 109 | function hasGraphqlData(data: unknown): boolean { | 155 | function 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 | } |
| @@ -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, |
| @@ -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 | |||