Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts",
"test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts && tsx src/workflow-contracts.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand All @@ -49,6 +49,7 @@
"diff": "^8.0.3",
"drizzle-orm": "^0.45.2",
"express": "^5.2.1",
"json-schema-to-ts": "^3.1.1",
"lucide": "^1.24.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
Expand Down
42 changes: 42 additions & 0 deletions src/json-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { JSONSchema } from "json-schema-to-ts";
import * as z from "zod/v4";

export type JsonPrimitive = string | number | boolean | null;

export type JsonValue =
| JsonPrimitive
| JsonValue[]
| { [key: string]: JsonValue };

export type JsonObject = { [key: string]: JsonValue };

/** JSON Schema is the portable contract shared with provider SDKs and Ajv. */
export type JsonSchema = JSONSchema;

export const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
z.union([
z.string(),
z.number().finite(),
z.boolean(),
z.null(),
z.array(jsonValueSchema),
z.record(z.string(), jsonValueSchema),
]),
);

export const jsonObjectSchema: z.ZodType<JsonObject> = z.record(
z.string(),
jsonValueSchema,
);

export const jsonSchemaSchema = jsonObjectSchema.transform(
(value): JsonSchema => value as JsonSchema,
);

export function parseJsonValue(value: unknown): JsonValue {
return jsonValueSchema.parse(value);
}

export function parseJsonText(text: string): JsonValue {
return parseJsonValue(JSON.parse(text) as unknown);
}
2 changes: 1 addition & 1 deletion src/local-agent-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ assert.equal(
type: "object",
properties: { n: { type: "number" } },
required: ["n"],
};
} as const;
assert.deepEqual(claudeOutputFormatOptions(schema), {
outputFormat: { type: "json_schema", schema },
});
Expand Down
10 changes: 7 additions & 3 deletions src/local-agent-adapters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
import { resolve } from "node:path";
import { Readable, Writable } from "node:stream";
import type { EffortLevel } from "@anthropic-ai/claude-agent-sdk";
import type {
EffortLevel,
OutputFormat,
} from "@anthropic-ai/claude-agent-sdk";
import type { JsonSchema } from "./json-types.js";
import type { LocalAgentProvider } from "./local-agent-profiles.js";
import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js";
import {
Expand Down Expand Up @@ -116,8 +120,8 @@ class ClaudeLocalAgentAdapter implements LocalAgentAdapter {

/** Build Claude SDK outputFormat when a JSON Schema is requested. */
export function claudeOutputFormatOptions(
schema: object | undefined,
): { outputFormat: { type: "json_schema"; schema: Record<string, unknown> } } | Record<string, never> {
schema: JsonSchema | undefined,
): { outputFormat: OutputFormat } | Record<string, never> {
if (!schema) return {};
return {
outputFormat: {
Expand Down
51 changes: 51 additions & 0 deletions src/local-agent-capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { LocalAgentProvider } from "./local-agent-profiles.js";

export interface LocalAgentProviderCapabilities {
structuredOutput: "native" | "prompt";
resumableSessions: boolean;
cancellation: "signal" | "process";
supportsWorkspaceIsolation: boolean;
}

export const LOCAL_AGENT_PROVIDER_CAPABILITIES = {
codex: {
structuredOutput: "native",
resumableSessions: true,
cancellation: "signal",
supportsWorkspaceIsolation: true,
},
claude: {
structuredOutput: "native",
resumableSessions: true,
cancellation: "signal",
supportsWorkspaceIsolation: true,
},
opencode: {
structuredOutput: "prompt",
resumableSessions: true,
cancellation: "process",
supportsWorkspaceIsolation: true,
},
pi: {
structuredOutput: "prompt",
resumableSessions: true,
cancellation: "process",
supportsWorkspaceIsolation: true,
},
cursor: {
structuredOutput: "prompt",
resumableSessions: true,
cancellation: "process",
supportsWorkspaceIsolation: true,
},
copilot: {
structuredOutput: "prompt",
resumableSessions: true,
cancellation: "process",
supportsWorkspaceIsolation: true,
},
} as const satisfies Record<LocalAgentProvider, LocalAgentProviderCapabilities>;

export function supportsNativeStructuredOutput(provider: LocalAgentProvider): boolean {
return LOCAL_AGENT_PROVIDER_CAPABILITIES[provider].structuredOutput === "native";
}
8 changes: 4 additions & 4 deletions src/local-agent-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { basename, join, resolve } from "node:path";
import { parse as parseYaml } from "yaml";
import type { ServerConfig } from "./config.js";

export type LocalAgentProvider = "codex" | "claude" | "opencode" | "pi" | "cursor" | "copilot";

export const LOCAL_AGENT_PROVIDERS: readonly LocalAgentProvider[] = [
export const LOCAL_AGENT_PROVIDERS = [
"codex",
"claude",
"opencode",
"pi",
"cursor",
"copilot",
];
] as const;

export type LocalAgentProvider = (typeof LOCAL_AGENT_PROVIDERS)[number];

export interface LocalAgentProfile {
name: string;
Expand Down
8 changes: 5 additions & 3 deletions src/local-agent-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
ThreadOptions,
TurnOptions,
} from "@openai/codex-sdk";
import type { JsonSchema } from "./json-types.js";
import type { LocalAgentProvider } from "./local-agent-profiles.js";

export type LocalAgentWriteMode = "read_only" | "allowed" | "full_access";

Expand All @@ -19,11 +21,11 @@ export interface LocalAgentRunInput {
/** Provider-native effort / reasoning level (was thinking). */
effort?: string;
/** JSON Schema for native structured output (codex/claude). */
schema?: object;
schema?: JsonSchema;
}

export interface LocalAgentRunResult {
provider: string;
provider: LocalAgentProvider;
providerSessionId: string | null;
finalResponse: string;
items: unknown[];
Expand All @@ -32,7 +34,7 @@ export interface LocalAgentRunResult {
}

export interface LocalAgentRuntime {
readonly provider: string;
readonly provider: LocalAgentProvider;
run(input: LocalAgentRunInput): Promise<LocalAgentRunResult>;
}

Expand Down
Loading
Loading