Skip to content

@ai-sdk/harness: host tool execute() never receives tool context (options.context is always undefined) #21075

Description

@ckvv

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:

  1. 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.

  2. 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:

  1. 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
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions