Skip to content
Draft
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
9 changes: 6 additions & 3 deletions src/graphs/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ export abstract class Graph<
* ToolNodes compiled from this graph captured the registry
* instance at construction time, so simply dropping the Graph's
* own reference would leave their captured reference — and every
* stored `tool<i>turn<n>` entry, plus up to `maxTotalSize` of raw
* output — alive across subsequent `processStream()` calls. Wipe
* the registry's contents first so subsequent runs start fresh.
* stored `tool<i>turn<n>` entry, plus up to `maxTotalSize` of
* retained output — alive across subsequent `processStream()`
* calls. Wipe the registry's contents first so subsequent runs
* start fresh.
*/
this._toolOutputRegistry?.clear();
this._toolOutputRegistry = undefined;
Expand Down Expand Up @@ -582,6 +583,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
maxContextTokens: agentContext?.maxContextTokens,
maxToolResultChars: agentContext?.maxToolResultChars,
toolOutputReferences: this.toolOutputReferences,
toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
errorHandler: (data, metadata) =>
StandardGraph.handleToolCallErrorStatic(this, data, metadata),
Expand Down Expand Up @@ -614,6 +616,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
sessions: this.sessions,
maxContextTokens: agentContext?.maxContextTokens,
maxToolResultChars: agentContext?.maxToolResultChars,
toolOutputReferences: this.toolOutputReferences,
toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
});
}
Expand Down
7 changes: 4 additions & 3 deletions src/tools/BashExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ Usage:
export const BashToolOutputReferencesGuide = `
Referencing previous tool outputs:
- Every successful tool result is tagged with a reference key of the form \`tool<idx>turn<turn>\` (e.g., \`tool0turn0\`). The key appears either as a \`[ref: tool0turn0]\` prefix line or, when the output is a JSON object, as a \`_ref\` field on the object.
- To pipe a previous tool output into this tool, embed the placeholder \`{{tool<idx>turn<turn>}}\` literally anywhere in the \`command\` string (or any string arg). It will be substituted with the stored output verbatim before the command runs.
- The substituted value is the original output string (no \`[ref: …]\` prefix, no \`_ref\` key), so it is safe to pipe directly into \`jq\`, \`grep\`, \`awk\`, etc.
- Example (simple ASCII output): \`echo '{{tool0turn0}}' | jq '.foo'\` takes the full output of the first tool from the first turn and pipes it into jq.
- To pipe a previous tool output into this tool, embed the placeholder \`{{tool<idx>turn<turn>}}\` literally anywhere in the \`command\` string (or any string arg). It will be substituted with the stored output before the command runs.
- By default, the substituted value is the full post-hook output string (subject to registry size caps), even when the LLM-visible tool result was truncated. It excludes the \`[ref: …]\` prefix and \`_ref\` key so it can be piped directly into \`jq\`, \`grep\`, \`awk\`, etc.
- If the host configured \`toolOutputReferences.referenceContent = "visible"\`, substitutions use the LLM-visible truncated content instead.
- Example (simple ASCII output): \`echo '{{tool0turn0}}' | jq '.foo'\` takes the stored output of the first tool from the first turn and pipes it into jq.
- For payloads that may contain quotes, parentheses, backticks, or arbitrary bytes (random/binary data, JSON with embedded quotes, multi-line strings), prefer a quoted-delimiter heredoc over \`echo '…'\`. The heredoc body is not interpreted by the shell, so substituted payloads pass through unchanged.
- Heredoc example: \`wc -c << 'EOF'\\n{{tool0turn0}}\\nEOF\` (the quotes around \`'EOF'\` disable interpolation inside the body).
- Unknown reference keys are left in place and surfaced as \`[unresolved refs: …]\` after the output.
Expand Down
58 changes: 40 additions & 18 deletions src/tools/ToolNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
private maxToolResultChars: number;
/** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
private hookRegistry?: HookRegistry;
/** Which output snapshot gets stored for later `{{…}}` substitutions. */
private toolOutputReferenceContent: t.ToolOutputReferenceContent = 'raw';
/**
* Registry of tool outputs keyed by `tool<idx>turn<turn>`.
*
Expand Down Expand Up @@ -227,16 +229,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
this.maxToolResultChars =
maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
this.hookRegistry = hookRegistry;
this.toolOutputReferenceContent =
toolOutputReferences?.referenceContent ?? 'raw';
/**
* Precedence: an explicitly passed `toolOutputRegistry` instance
* wins over a config object so a host (`Graph`) can share one
* registry across many ToolNodes. When only the config is
* provided (direct ToolNode usage), build a local registry so
* the feature still works without graph-level plumbing. Registry
* caps are intentionally decoupled from `maxToolResultChars`:
* the registry stores the raw untruncated output so a later
* `{{…}}` substitution pipes the full payload into the next
* tool, even when the LLM saw a truncated preview.
* the feature still works without graph-level plumbing. The
* registry retains whichever snapshot `referenceContent` selects:
* raw for full parser/piping inputs, or visible for stricter
* deployments that treat truncation as a data boundary.
*/
if (toolOutputRegistry != null) {
this.toolOutputRegistry = toolOutputRegistry;
Expand All @@ -262,6 +265,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
return this.toolOutputRegistry;
}

private getReferenceContent(raw: string, visible: string): string {
if (this.toolOutputReferenceContent === 'visible') {
return visible;
}
return raw;
}

/**
* Returns cached programmatic tools, computing once on first access.
* Single iteration builds both toolMap and toolDefs simultaneously.
Expand Down Expand Up @@ -472,7 +482,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
toolMsg.content = llmContent;
const refMeta = this.recordOutputReference(
runId,
rawContent,
this.getReferenceContent(rawContent, llmContent),
refKey,
unresolvedRefs
);
Expand Down Expand Up @@ -521,7 +531,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
);
const refMeta = this.recordOutputReference(
runId,
rawContent,
this.getReferenceContent(rawContent, truncated),
refKey,
unresolvedRefs
);
Expand Down Expand Up @@ -601,16 +611,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
}

/**
* Registers the full, raw output under `refKey` (when provided) and
* Registers the selected output snapshot under `refKey` (when provided) and
* builds the per-message ref metadata stamped onto the resulting
* `ToolMessage.additional_kwargs`. The metadata is read at LLM-
* request time by `annotateMessagesForLLM` to produce a transient
* annotated copy of the message — the persisted `content` itself
* stays clean.
*
* @param registryContent The full, untruncated output to store in
* the registry so `{{tool<i>turn<n>}}` substitutions deliver the
* complete payload. Ignored when `refKey` is undefined.
* @param registryContent The output snapshot to store in the
* registry for `{{tool<i>turn<n>}}` substitutions. This is either
* the full post-hook raw output or the LLM-visible content,
* depending on `toolOutputReferences.referenceContent`. Ignored
* when `refKey` is undefined.
* @param refKey Precomputed `tool<i>turn<n>` key, or undefined when
* the output is not to be registered (errors, disabled feature,
* unavailable batch/turn).
Expand Down Expand Up @@ -697,10 +709,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
}

const request = requestMap.get(result.toolCallId);
const requestName = request?.name;
if (
!request?.name ||
(!CODE_EXECUTION_TOOLS.has(request.name) &&
request.name !== Constants.SKILL_TOOL)
requestName == null ||
requestName === '' ||
(!CODE_EXECUTION_TOOLS.has(requestName) &&
requestName !== Constants.SKILL_TOOL)
) {
continue;
}
Expand Down Expand Up @@ -1140,14 +1154,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
});
}
} else {
let registryRaw =
let rawContent =
typeof result.content === 'string'
? result.content
: JSON.stringify(result.content);
contentString = truncateToolResultContent(
registryRaw,
rawContent,
this.maxToolResultChars
);
let registryContent = this.getReferenceContent(
rawContent,
contentString
);

if (hasPostHook) {
const hookResult = await executeHooks({
Expand All @@ -1172,11 +1190,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
typeof hookResult.updatedOutput === 'string'
? hookResult.updatedOutput
: JSON.stringify(hookResult.updatedOutput);
registryRaw = replaced;
contentString = truncateToolResultContent(
replaced,
this.maxToolResultChars
);
rawContent = replaced;
registryContent = this.getReferenceContent(
rawContent,
contentString
);
}
}

Expand All @@ -1190,7 +1212,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
: undefined;
const successRefMeta = this.recordOutputReference(
registryRunId,
registryRaw,
registryContent,
refKey,
unresolved
);
Expand Down
Loading
Loading