diff options
| author | Adam Malczewski <[email protected]> | 2026-06-05 21:19:45 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-05 21:19:45 +0900 |
| commit | 4283d1f8a0bc3953e65962a2364c903d0015f047 (patch) | |
| tree | 4de5d2d2c1114301f03236b6dfc98a638a7c61c0 /packages/kernel/src | |
| parent | 368be032ef57638b558db659d70bfac00cb95cdd (diff) | |
| download | dispatch-4283d1f8a0bc3953e65962a2364c903d0015f047.tar.gz dispatch-4283d1f8a0bc3953e65962a2364c903d0015f047.zip | |
feat(kernel): listModels/ModelInfo + per-turn cwd contracts; add transport-contract wire package
Diffstat (limited to 'packages/kernel/src')
| -rw-r--r-- | packages/kernel/src/contracts/index.ts | 1 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/provider.ts | 20 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/runtime.ts | 9 | ||||
| -rw-r--r-- | packages/kernel/src/contracts/tool.ts | 8 | ||||
| -rw-r--r-- | packages/kernel/src/runtime/dispatch.ts | 4 | ||||
| -rw-r--r-- | packages/kernel/src/runtime/run-turn.test.ts | 65 | ||||
| -rw-r--r-- | packages/kernel/src/runtime/run-turn.ts | 3 |
7 files changed, 110 insertions, 0 deletions
diff --git a/packages/kernel/src/contracts/index.ts b/packages/kernel/src/contracts/index.ts index a4c965a..6f6954e 100644 --- a/packages/kernel/src/contracts/index.ts +++ b/packages/kernel/src/contracts/index.ts @@ -83,6 +83,7 @@ export type { } from "./logging.js"; export type { FinishEvent, + ModelInfo, ProviderContract, ProviderErrorEvent, ProviderEvent, diff --git a/packages/kernel/src/contracts/provider.ts b/packages/kernel/src/contracts/provider.ts index 62dc8e9..ee58c1d 100644 --- a/packages/kernel/src/contracts/provider.ts +++ b/packages/kernel/src/contracts/provider.ts @@ -104,6 +104,17 @@ export interface ProviderStreamOptions { } /** + * Metadata describing a single model a provider can serve. Returned by + * `listModels` so a catalog (e.g. the credential-store) can enumerate the + * `<credentialName>/<model>` choices a client may select. Kept minimal — `id` + * is the wire model identifier; `displayName` is an optional human label. + */ +export interface ModelInfo { + readonly id: string; + readonly displayName?: string; +} + +/** * What a provider extension registers with the kernel. The kernel calls * `stream` and consumes the async iterable of events — it never knows which * concrete LLM API is behind it. @@ -122,4 +133,13 @@ export interface ProviderContract { tools: readonly ToolContract[], opts?: ProviderStreamOptions, ) => AsyncIterable<ProviderEvent>; + + /** + * Enumerate the models this provider can serve, each in its own way (e.g. an + * OpenAI-compatible provider GETs `/v1/models`). Optional: a provider that + * cannot (or chooses not to) enumerate omits it, and a catalog simply lists + * none for it. A future multi-credential design may pass per-credential + * credentials in; today the provider uses the key it resolved at activate. + */ + readonly listModels?: () => Promise<readonly ModelInfo[]>; } diff --git a/packages/kernel/src/contracts/runtime.ts b/packages/kernel/src/contracts/runtime.ts index 1e8f14f..8917709 100644 --- a/packages/kernel/src/contracts/runtime.ts +++ b/packages/kernel/src/contracts/runtime.ts @@ -76,6 +76,15 @@ export interface RunTurnInput { readonly signal?: AbortSignal; /** + * Working directory for this turn's tool execution. The kernel does NOT + * interpret it — it forwards the value verbatim to each `ToolExecuteContext.cwd` + * so tools resolve/contain paths against it. It never enters the model prompt, + * so it does not affect prompt caching. When omitted, tools fall back to their + * own configured/default workdir. + */ + readonly cwd?: string; + + /** * Optional logger for structured span instrumentation. The runtime opens * turn/step/tool-call spans using this logger. If omitted, no spans are * emitted (backward-compatible with callers that don't yet pass a logger). diff --git a/packages/kernel/src/contracts/tool.ts b/packages/kernel/src/contracts/tool.ts index 0699d05..f617f42 100644 --- a/packages/kernel/src/contracts/tool.ts +++ b/packages/kernel/src/contracts/tool.ts @@ -62,6 +62,14 @@ export interface ToolExecuteContext { * turnId, and spanId automatically. */ readonly log: Logger; + + /** + * Working directory for this turn, forwarded verbatim from `RunTurnInput.cwd`. + * Tools that touch the filesystem resolve and contain paths against it. + * Optional: when omitted, a tool falls back to its own configured/default + * workdir. The kernel never interprets it. + */ + readonly cwd?: string; } /** diff --git a/packages/kernel/src/runtime/dispatch.ts b/packages/kernel/src/runtime/dispatch.ts index 1ba0849..d168319 100644 --- a/packages/kernel/src/runtime/dispatch.ts +++ b/packages/kernel/src/runtime/dispatch.ts @@ -17,6 +17,7 @@ export async function executeToolCall( conversationId: string, turnId: string, toolSpan?: Span, + cwd?: string, ): Promise<ToolResult> { if (tool === undefined) { return { content: `Unknown tool: ${call.name}`, isError: true }; @@ -31,6 +32,7 @@ export async function executeToolCall( emit(toolOutputEvent(conversationId, turnId, call.id, data, stream)); }, log: toolSpan?.log ?? createNoopLogger(), + ...(cwd !== undefined ? { cwd } : {}), }; try { return await tool.execute(call.input, ctx); @@ -54,6 +56,7 @@ export function createStepDispatcher( conversationId: string, turnId: string, toolSpans: Map<string, Span>, + cwd?: string, ): StepDispatcher { let activeCount = 0; let unsafeRunning = false; @@ -91,6 +94,7 @@ export function createStepDispatcher( conversationId, turnId, tcSpan, + cwd, ); activeCount--; if (entry.tool?.concurrencySafe === false) unsafeRunning = false; diff --git a/packages/kernel/src/runtime/run-turn.test.ts b/packages/kernel/src/runtime/run-turn.test.ts index 2dd5d2e..48a6fbc 100644 --- a/packages/kernel/src/runtime/run-turn.test.ts +++ b/packages/kernel/src/runtime/run-turn.test.ts @@ -740,6 +740,71 @@ describe("runTurn", () => { } }); + it("forwards cwd from RunTurnInput to ToolExecuteContext", async () => { + let capturedCwd: string | undefined = "SENTINEL_NOT_SET"; + + const tool = createFakeTool("cwdcheck", async (_input, ctx) => { + capturedCwd = ctx.cwd; + return { content: "ok" }; + }); + + const provider = createFakeProvider([ + [ + { type: "tool-call", toolCallId: "tc1", toolName: "cwdcheck", input: {} }, + { type: "finish", reason: "tool-calls" }, + ], + [ + { type: "text-delta", delta: "done" }, + { type: "finish", reason: "stop" }, + ], + ]); + + await runTurn({ + provider, + messages: [userMessage], + tools: [tool], + dispatch: { maxConcurrent: 1, eager: false }, + conversationId: "tab-test", + turnId: "turn-test", + emit: () => {}, + cwd: "/some/dir", + }); + + expect(capturedCwd).toBe("/some/dir"); + }); + + it("forwards undefined cwd when RunTurnInput has no cwd", async () => { + let capturedCwd: string | undefined = "SENTINEL_NOT_SET"; + + const tool = createFakeTool("cwdcheck", async (_input, ctx) => { + capturedCwd = ctx.cwd; + return { content: "ok" }; + }); + + const provider = createFakeProvider([ + [ + { type: "tool-call", toolCallId: "tc1", toolName: "cwdcheck", input: {} }, + { type: "finish", reason: "tool-calls" }, + ], + [ + { type: "text-delta", delta: "done" }, + { type: "finish", reason: "stop" }, + ], + ]); + + await runTurn({ + provider, + messages: [userMessage], + tools: [tool], + dispatch: { maxConcurrent: 1, eager: false }, + conversationId: "tab-test", + turnId: "turn-test", + emit: () => {}, + }); + + expect(capturedCwd).toBeUndefined(); + }); + it("aggregates usage across multiple steps", async () => { const provider = createFakeProvider([ [ diff --git a/packages/kernel/src/runtime/run-turn.ts b/packages/kernel/src/runtime/run-turn.ts index 01b280b..6b07e24 100644 --- a/packages/kernel/src/runtime/run-turn.ts +++ b/packages/kernel/src/runtime/run-turn.ts @@ -80,6 +80,7 @@ interface StepContext { readonly logger: Logger; readonly turnSpan: Span | undefined; readonly toolSpans: Map<string, Span>; + readonly cwd: string | undefined; } interface StepResult { @@ -202,6 +203,7 @@ async function executeStep(ctx: StepContext): Promise<StepResult> { ctx.conversationId, ctx.turnId, ctx.toolSpans, + ctx.cwd, ); try { @@ -364,6 +366,7 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> { logger: turnSpan?.log ?? logger ?? createNoopLogger(), turnSpan, toolSpans, + cwd: input.cwd, }); totalUsage = addUsage(totalUsage, stepResult.usage); |
