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 | 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 | 386 | runtimeCallbacks: { |
| 247 | 387 | completeRun: FunctionReference< |
| 248 | 388 | "mutation", |
| @@ -1219,6 +1359,18 @@ export declare const internal: { · 970 unmodified lines | ||
| 1219 | 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 | 1374 | toolRegistry: { |
| 1223 | 1375 | beginBuild: FunctionReference< |
| 1224 | 1376 | "mutation", |
| @@ -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"; | |
| @@ -1,5 +1,19 @@ | ||
| 1 | +import { Result, TaggedError } from "better-result"; | |
| 1 | 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 | 17 | export interface OpenApiRequestRunSpec { |
| 4 | 18 | baseUrl: string; |
| 5 | 19 | method: string; |
| @@ -73,7 +87,7 @@ export async function executeOpenApiRequest( · 67 unmodified lines | ||
| 73 | 87 | runSpec: OpenApiRequestRunSpec, |
| 74 | 88 | input: unknown, |
| 75 | 89 | credentialHeaders?: Record<string, string>, |
| 76 | −): Promise<unknown> { | |
| 90 | +): Promise<Result<unknown, OpenApiRequestError>> { | |
| 77 | 91 | const payload = asRecord(input); |
| 78 | 92 | const readMethods = new Set(["get", "head", "options"]); |
| 79 | 93 | const { url, bodyInput } = buildOpenApiUrl( |
| @@ -84,7 +98,7 @@ export async function executeOpenApiRequest( · 4 unmodified lines | ||
| 84 | 98 | ); |
| 85 | 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 | 102 | method: runSpec.method.toUpperCase(), |
| 89 | 103 | headers: { |
| 90 | 104 | ...runSpec.authHeaders, |
| @@ -92,18 +106,50 @@ export async function executeOpenApiRequest( · 1 unmodified line | ||
| 92 | 106 | ...(hasBody ? { "content-type": "application/json" } : {}), |
| 93 | 107 | }, |
| 94 | 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 | 128 | 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 | + })); | |
| 100 | 136 | } |
| 101 | 137 | |
| 102 | 138 | 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 | + })); | |
| 105 | 150 | } |
| 106 | − return await response.text(); | |
| 151 | + | |
| 152 | + return Result.ok(bodyResult.value); | |
| 107 | 153 | } |
| 108 | 154 | |
| 109 | 155 | function hasGraphqlData(data: unknown): boolean { |
| @@ -126,8 +172,8 @@ export async function executeGraphqlRequest( · 16 unmodified lines | ||
| 126 | 172 | query: string, |
| 127 | 173 | variables: unknown, |
| 128 | 174 | 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, { | |
| 131 | 177 | method: "POST", |
| 132 | 178 | headers: { |
| 133 | 179 | "content-type": "application/json", |
| @@ -135,17 +181,41 @@ export async function executeGraphqlRequest( · 1 unmodified line | ||
| 135 | 181 | ...(credentialHeaders ?? {}), |
| 136 | 182 | }, |
| 137 | 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 | 195 | 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 | + })); | |
| 143 | 211 | } |
| 144 | 212 | |
| 145 | − const result = await response.json() as { data?: unknown; errors?: unknown[] }; | |
| 213 | + const decoded = result.value; | |
| 146 | 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 | 15 | "agent":{ |
| 16 | − | |
| 17 | 16 | "quick-build": { |
| 18 | 17 | "mode": "primary", |
| 18 | + "description": "A smaller context winder but extremely fast at working", | |
| 19 | 19 | "model": "openai/gpt-5.3-codex-spark", |
| 20 | 20 | "tools": { |
| 21 | 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 | + | |