From b0114ef5fabe1534b2b2f2ff5eba3604b5aa8fb0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 6 Oct 2026 17:14:43 +0200 Subject: [PATCH] feat(durable): expose models on ToolExecutionApi and HookApi closes #10395 --- packages/durable/CHANGELOG.md | 4 ++++ packages/durable/docs/spec.md | 4 ++++ packages/durable/src/harness/tool.ts | 1 + packages/durable/src/harness/types.ts | 4 ++++ packages/durable/test/harness-tools.test.ts | 24 +++++++++++++++++++++ 5 files changed, 37 insertions(+) diff --git a/packages/durable/CHANGELOG.md b/packages/durable/CHANGELOG.md index d0b27a67c..f049c434a 100644 --- a/packages/durable/CHANGELOG.md +++ b/packages/durable/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `ToolExecutionApi.models` and `HookApi.models`: the Harness's `models`, so tools and hooks can resolve models and make requests with the same catalog, credentials, and request transforms as generation ([#10395](https://github.com/earendil-works/pi/issues/10395)). + ### Changed - Within one task invocation, `runtime.context()` reuses the range its previous read scanned and reads only entries committed since then, unless the head marker changed. A generation's request no longer rereads the whole transcript after its prepare, which roughly halves context reads per turn. The range is dropped when the invocation ends or sleeps. diff --git a/packages/durable/docs/spec.md b/packages/durable/docs/spec.md index 59c0629a2..bb6cec74f 100644 --- a/packages/durable/docs/spec.md +++ b/packages/durable/docs/spec.md @@ -2561,6 +2561,8 @@ type HookResult = T | undefined | Promise; interface HookApi extends DocumentReader { readonly taskId: TaskId; readonly conversationId: ConversationId; + /** `HarnessOptions.models`. */ + readonly models: Models; memo(name: string, context: Context): Promise; memo(name: string, candidate: T, context: Context): Promise; } @@ -2693,6 +2695,8 @@ interface ToolExecutionApi extends Docum readonly registry: RegistrySnapshot; /** The calling conversation's agent, as the tool task's phase resolved it. */ agent(context: Context): Promise; + /** `HarnessOptions.models`: the catalog, credentials, and request transforms generation uses. */ + readonly models: Models; /** Built by `HarnessOptions.env` for this call. */ readonly env: ExecutionEnv | undefined; output(chunk: string | Uint8Array): void; diff --git a/packages/durable/src/harness/tool.ts b/packages/durable/src/harness/tool.ts index 4c84ff062..095793ce9 100644 --- a/packages/durable/src/harness/tool.ts +++ b/packages/durable/src/harness/tool.ts @@ -190,6 +190,7 @@ async function run( callId: call.id, registry: runtime.registry, agent: runtime.agent, + models: runtime.models, output: (chunk, skipped) => { assertLive(); if (reported.output.push(chunk, skipped)) progress.mark(); diff --git a/packages/durable/src/harness/types.ts b/packages/durable/src/harness/types.ts index 941a3daa4..a3344d2f9 100644 --- a/packages/durable/src/harness/types.ts +++ b/packages/durable/src/harness/types.ts @@ -171,6 +171,8 @@ export interface ToolExecutionApi extend readonly registry: RegistrySnapshot; /** The calling conversation's agent, as the tool task's phase resolved it. */ agent(context: Context): Promise; + /** `HarnessOptions.models`: the catalog, credentials, and request transforms generation uses. */ + readonly models: Models; /** Built by `HarnessOptions.env` for this call; `undefined` without an environment. */ readonly env: ExecutionEnv | undefined; /** @@ -605,6 +607,8 @@ export interface Harness extends Session { export interface HookApi extends DocumentReader { readonly taskId: TaskId; readonly conversationId: ConversationId; + /** `HarnessOptions.models`. */ + readonly models: Models; memo(name: string, context: Context): Promise; memo(name: string, candidate: T, context: Context): Promise; } diff --git a/packages/durable/test/harness-tools.test.ts b/packages/durable/test/harness-tools.test.ts index 82b6e3794..88cbdbd37 100644 --- a/packages/durable/test/harness-tools.test.ts +++ b/packages/durable/test/harness-tools.test.ts @@ -111,6 +111,30 @@ describe("tool round", () => { await harness.close(context); }); + // #10395 + it("gives tools and hooks the Harness's models", async () => { + const setup = chatSetup(); + const seen: unknown[] = []; + addTool( + setup.registry, + tool("echo", async (_args, api) => { + seen.push(api.models); + return { content: [] }; + }), + ); + addHooks(setup.registry, ToolTask, { + beforeTool: (_call, api) => { + seen.push(api.models); + return undefined; + }, + }); + const { harness, status } = await run(setup, [calls(["echo", {}, "c1"]), DONE]); + expect(status).toBe("done"); + expect(seen).toEqual([setup.models, setup.models]); + expect(seen.every((models) => models === setup.models)).toBe(true); + await harness.close(context); + }); + it("answers calls to tools the request did not offer without a task", async () => { const setup = chatSetup(); addTool(