Summary
Host tools declared with a contextSchema never receive their context when executed by HarnessAgent: execute(input, options) is called with options.context === undefined. The same tool receives the expected context with streamText + toolsContext (and with ToolLoopAgent), so a tool definition that needs request-scoped identity has to be duplicated as a per-request closure factory to work with harness runtimes.
Environment
@ai-sdk/harness@1.0.115
@ai-sdk/harness-pi@1.0.117
ai@7.0.105 (@ai-sdk/provider-utils@5.0.43)
- Node 24, pnpm
Reproduction
const probe = tool({
inputSchema: z.object({ value: z.string() }),
contextSchema: z.object({ userId: z.string() }),
execute: async (input, { context }) => {
console.log(context); // HarnessAgent: undefined / streamText + toolsContext: { userId: 'u1' }
return { ok: true };
},
});
const agent = new HarnessAgent({ harness, sandbox, tools: { probe } });
// Ask the model to call `probe` → options.context is undefined.
Under streamText the same tool is invoked with { userId: 'u1' } when passing toolsContext: { probe: { userId: 'u1' } }.
Root cause
Both places that would carry the tool context are hardcoded to empty:
-
The turn's tool context is created empty and there is no way for the caller to populate it (packages/harness/src/agent/internal/run-prompt.ts:150):
const toolsContext = {} as InferToolSetContext<TOOLS>;
That empty object is what HarnessStreamTextResult and the turn lifecycle receive, so step results and telemetry report toolsContext: {} as well.
-
maybeExecuteHostTool passes undefined as the tool context (packages/harness/src/agent/internal/run-prompt.ts:1469):
const stream = executeTool({
tool,
input: args as never,
options: {
toolCallId: input.event.toolCallId,
messages: [],
abortSignal: input.abortSignal,
context: undefined as never,
experimental_sandbox: input.sandboxSession,
},
});
executeTool from @ai-sdk/provider-utils only forwards options to tool.execute(input, options), so nothing fills the gap. There is also no contextSchema validation on this path, so a tool that reads context.userId fails with a TypeError inside execute instead of a context validation error.
The non-harness path does the opposite: executeToolCall resolves the per-tool context from toolsContext[toolName], validates it against the tool's contextSchema, and passes it as options.context.
Expected behavior
Host tools should receive the same per-tool context as they do everywhere else in the AI SDK. Either shape works:
- a
toolsContext setting on HarnessAgentSettings (or a field returned from the existing prepareCall hook) that maybeExecuteHostTool forwards as context: toolsContext[toolName], reusing the existing contextSchema validation; or
- a documented per-call hook for binding context, if harness host tools are intentionally meant to be per-request closures.
prepareCall can already swap tools between turns, so option 1 would sit naturally next to it.
Impact
Any host tool that needs request-scoped identity (user / tenant / space id, message id, a stream writer for streaming UI parts, a per-request citation registry, DB session options) cannot be reused with HarnessAgent as-is. Consumers either duplicate the tool as a closure factory — including its carefully written description, which then drifts from the shared definition — or they cannot use the tool at all.
Workaround
Bind the context per request before handing the tool set to the agent:
const boundTools = Object.fromEntries(
Object.entries(tools).map(([name, t]) => [
name,
{
...t,
execute: (input, options) => t.execute(input, { ...options, context }),
},
]),
);
new HarnessAgent({ harness, sandbox, tools: boundTools });
(prepareCall works too if the context is per call rather than per agent instance.)
Related
Summary
Host tools declared with a
contextSchemanever receive their context when executed byHarnessAgent:execute(input, options)is called withoptions.context === undefined. The same tool receives the expected context withstreamText+toolsContext(and withToolLoopAgent), so a tool definition that needs request-scoped identity has to be duplicated as a per-request closure factory to work with harness runtimes.Environment
@ai-sdk/harness@1.0.115@ai-sdk/harness-pi@1.0.117ai@7.0.105(@ai-sdk/provider-utils@5.0.43)Reproduction
Under
streamTextthe same tool is invoked with{ userId: 'u1' }when passingtoolsContext: { probe: { userId: 'u1' } }.Root cause
Both places that would carry the tool context are hardcoded to empty:
The turn's tool context is created empty and there is no way for the caller to populate it (
packages/harness/src/agent/internal/run-prompt.ts:150):That empty object is what
HarnessStreamTextResultand the turn lifecycle receive, so step results and telemetry reporttoolsContext: {}as well.maybeExecuteHostToolpassesundefinedas the tool context (packages/harness/src/agent/internal/run-prompt.ts:1469):executeToolfrom@ai-sdk/provider-utilsonly forwardsoptionstotool.execute(input, options), so nothing fills the gap. There is also nocontextSchemavalidation on this path, so a tool that readscontext.userIdfails with aTypeErrorinsideexecuteinstead of a context validation error.The non-harness path does the opposite:
executeToolCallresolves the per-tool context fromtoolsContext[toolName], validates it against the tool'scontextSchema, and passes it asoptions.context.Expected behavior
Host tools should receive the same per-tool context as they do everywhere else in the AI SDK. Either shape works:
toolsContextsetting onHarnessAgentSettings(or a field returned from the existingprepareCallhook) thatmaybeExecuteHostToolforwards ascontext: toolsContext[toolName], reusing the existingcontextSchemavalidation; orprepareCallcan already swaptoolsbetween turns, so option 1 would sit naturally next to it.Impact
Any host tool that needs request-scoped identity (user / tenant / space id, message id, a stream writer for streaming UI parts, a per-request citation registry, DB session options) cannot be reused with
HarnessAgentas-is. Consumers either duplicate the tool as a closure factory — including its carefully writtendescription, which then drifts from the shared definition — or they cannot use the tool at all.Workaround
Bind the context per request before handing the tool set to the agent:
(
prepareCallworks too if the context is per call rather than per agent instance.)Related
toolApprovalcallbacks for host tools. The PR description says callbacks are evaluated with host-visibletoolsContextandruntimeContext, so this plumbing is already being touched for approval; this issue is the execution counterpart.execute()or the model (same family: host tool options are lossy).context: undefinedlives.