Skip to content
Merged
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
51 changes: 51 additions & 0 deletions docs/subagent-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,54 @@ inherited runtime hints or direct tool implementations from bypassing that
workspace. Published files and any private artifact recovery remain the host's
responsibility. The host must reconstruct its adapter and authorization when resuming. The SDK
does not replay private artifacts to the host when restoring a checkpoint.

## Startup diagnostics

Pass `onSubagentResolutionFailure` to `Run.create` or a graph input. Direct
`SubagentExecutor` callers use `onResolutionFailure`. Detached executors and
nested child graphs inherit the callback.

The synchronous callback receives:

- A read-only diagnostic: `phase` (`identity` or `config`), `subagentType`,
`aborted`, a closed error `type`, and an SDK-owned `cause` and `message`.
- Correlation IDs: `parentRunId`, `parentAgentId`, `parentToolCallId`, parent
`threadId`, `childRunId`, `childThreadId`, and detached `taskId` when available.
- The original rejection as a **private second argument**. Use trusted error
classes or codes to classify it. Never log its message, name, stack, or body.

Return one of `workspace_unavailable`, `agent_unavailable`, `model_unavailable`,
`configuration_changed`, or `unknown`. The SDK builds the parent message from
fixed text; arbitrary runtime return values also fall back to `unknown`.
The input diagnostic starts with `cause: 'unknown'`. Log the mapped cause and
`getSubagentResolutionFailureMessage(cause)` in the host callback:

```ts
onSubagentResolutionFailure: (detail, error) => {
const cause = classifyStartupError(error); // Host-owned class/code mapping.
logger.log(detail.aborted ? 'warn' : 'error', 'Subagent resolution failed', {
...detail,
conversationId,
cause,
message: getSubagentResolutionFailureMessage(cause),
});
return cause;
},
```

A callback returning nothing preserves the unknown fallback. If no callback is
provided, or it throws, the SDK uses `console.warn` with only safe diagnostics.
A callback failure never replaces the child failure.

Direct executor results include `resolutionFailure: { phase, cause }`. Detached
execution throws `SubagentResolutionError` with the same safe `phase`,
`resolutionCause`, and fixed message. Hosts can recognize this class when
mapping background failures instead of exposing arbitrary `Error.message`.
The SDK does not infer host-specific workspace, agent-access, or provider
failures from free text. Those mappings belong to the host.

Graph replay preparation uses the same diagnostic boundary before invoking the
subagent tool. Ordinary preparation failures return a safe error `ToolMessage`
without starting child work. Framework interrupts and stream limits retain their
control-flow semantics. Aborted preparation and settlement identity failures
throw `SubagentResolutionError`; neither exposes the original rejection.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/agents",
"version": "4.0.1",
"version": "4.0.2",
"reova": {
"enabled": true,
"endpoint": "https://telemetry.reo.dev/data"
Expand Down
4 changes: 4 additions & 0 deletions src/graphs/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1369,6 +1369,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
/** See {@link t.StandardGraphInput.subagentExecutionContext}. */
readonly subagentExecutionContext?: t.SubagentExecutionContext;
private readonly subagentContext?: t.SubagentContextAdapter;
private readonly onSubagentResolutionFailure?: t.StandardGraphInput['onSubagentResolutionFailure'];
/** See {@link t.StandardGraphInput.preemption}. */
preemption?: t.StreamPreemption;
/**
Expand Down Expand Up @@ -1547,6 +1548,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
subagentScope,
subagentExecutionContext,
subagentContext,
onSubagentResolutionFailure,
preemption,
streamLimits,
toolExecution,
Expand Down Expand Up @@ -1575,6 +1577,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
this.subagentScope = subagentScope === true;
this.subagentExecutionContext = subagentExecutionContext;
this.subagentContext = subagentContext;
this.onSubagentResolutionFailure = onSubagentResolutionFailure;
this.preemption = preemption;
this.streamLimits = resolveStreamLimits(streamLimits);
this.toolExecution = toolExecution;
Expand Down Expand Up @@ -5342,6 +5345,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
usageSink: this.subagentUsageSink,
taskConfig: this.subagentTasks,
subagentContext: this.subagentContext,
onResolutionFailure: this.onSubagentResolutionFailure,
streamLimits: this.streamLimits,
humanInTheLoop: this.humanInTheLoop,
checkpointer: this.compileOptions?.checkpointer,
Expand Down
4 changes: 4 additions & 0 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ export class Run<_T extends t.BaseGraphState> {
private streamLimits?: t.StreamLimits;
private subagentTasks?: t.SubagentTaskConfig;
private subagentContext?: t.SubagentContextAdapter;
private onSubagentResolutionFailure?: t.StandardGraphInput['onSubagentResolutionFailure'];
private indexTokenCountMap?: Record<string, number>;
calibrationRatio: number = 1;
fadingTier?: t.FadingTier;
Expand Down Expand Up @@ -571,6 +572,7 @@ export class Run<_T extends t.BaseGraphState> {
this.subagentUsageSink = config.subagentUsageSink;
this.subagentTasks = config.subagentTasks;
this.subagentContext = config.subagentContext;
this.onSubagentResolutionFailure = config.onSubagentResolutionFailure;
this.preemption = config.preemption;
this.maxStopContinuations = resolveMaxStopContinuations(
config.maxStopContinuations
Expand Down Expand Up @@ -673,6 +675,7 @@ export class Run<_T extends t.BaseGraphState> {
subagentUsageSink: this.subagentUsageSink,
subagentTasks: this.subagentTasks,
subagentContext: this.subagentContext,
onSubagentResolutionFailure: this.onSubagentResolutionFailure,
preemption: this.preemption,
streamLimits: this.streamLimits,
toolExecution: this.toolExecution,
Expand Down Expand Up @@ -719,6 +722,7 @@ export class Run<_T extends t.BaseGraphState> {
subagentUsageSink: this.subagentUsageSink,
subagentTasks: this.subagentTasks,
subagentContext: this.subagentContext,
onSubagentResolutionFailure: this.onSubagentResolutionFailure,
preemption: this.preemption,
streamLimits: this.streamLimits,
toolExecution: this.toolExecution,
Expand Down
21 changes: 21 additions & 0 deletions src/tools/subagent/SubagentExecutionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export class SubagentExecutionRecord<
TSettledOutput = never,
> {
private identityValue?: SubagentExecutionIdentity;
private attemptedIdentityValue?: SubagentExecutionIdentity;
private bindingValue?: SubagentDefinitionBinding;
private bindingAuthority?: 'provisional' | 'effective';
private invocationValue?: SubagentInvocationBinding;
Expand Down Expand Up @@ -488,6 +489,24 @@ export class SubagentExecutionRecord<
return this.identityValue;
}

/** Correlation only; never committed identity or resume authority. */
get attemptedIdentity(): SubagentExecutionIdentity | undefined {
return this.attemptedIdentityValue;
}

recordIdentityAttempt(identity: SubagentExecutionIdentity): void {
const current = this.attemptedIdentityValue;
if (
current != null &&
current.childRunId === identity.childRunId &&
current.childThreadId === identity.childThreadId &&
current.approvalExecutionScope === identity.approvalExecutionScope
) {
return;
}
this.attemptedIdentityValue = Object.freeze({ ...identity });
}

get binding(): SubagentDefinitionBinding | undefined {
return this.bindingValue;
}
Expand Down Expand Up @@ -570,6 +589,7 @@ export class SubagentExecutionRecord<
if (this.pendingIdentityResolution != null) {
return this.pendingIdentityResolution;
}
this.attemptedIdentityValue = undefined;
const pending = Promise.resolve()
.then(() => {
this.assertCurrentIdentityResolution(pending);
Expand Down Expand Up @@ -929,6 +949,7 @@ export class SubagentExecutionRegistry<
if (this.recordsByAddress.get(record.address.key) !== record) {
throw new SubagentExecutionInvalidatedError();
}
record.recordIdentityAttempt(identity);
const checkpointKey = getPreparationResourceKey(
'checkpoint',
identity.childThreadId
Expand Down
Loading
Loading