Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c66dc21
feat: support foreground graph subagent approvals and resume
lia-librechat Oct 5, 2026
704f27c
test: retain graph host argument rejection with revision support
lia-librechat Oct 5, 2026
6e0f551
fix: Bind Graph Resume to Tool Capabilities
lia-librechat Oct 5, 2026
a55b691
fix: Bind Graph Resume to Member Session Partitions
lia-librechat Oct 5, 2026
e4adb46
fix: Restore Graph Approval Evidence Per Member
lia-librechat Oct 5, 2026
48c821f
fix: Preserve Settled Graph Effects During Partial Resume
lia-librechat Oct 5, 2026
3cc511a
fix: Bind Functional Prompts and Restore Refreshed Sessions
lia-librechat Oct 5, 2026
421f7ef
Merge remote-tracking branch 'origin/main' into lia/graph-subagent-hitl
lia-librechat Oct 17, 2026
48454f6
fix: Expose Pending Graph Approvals and Bind Tool Discovery
lia-librechat Oct 17, 2026
6e5683b
Merge remote-tracking branch 'origin/main' into lia/graph-subagent-hitl
lia-librechat Oct 22, 2026
61d70d0
fix: Bind Model Runtime Options and Isolate Question Replay
lia-librechat Oct 22, 2026
9aa4d11
fix: Bind Member Execution and Preserve Custom Interrupts
lia-librechat Oct 22, 2026
ce0c9bb
fix: Bind Cached Tool Schema Budgets During Resume
lia-librechat Oct 22, 2026
1d02555
fix: Restore Root Parallel Interrupt Evidence
lia-librechat Oct 23, 2026
372c280
fix: Scope Parent Replay Records and Child Manifests
lia-librechat Oct 23, 2026
498179b
fix: Merge Question Parent State and Enumerate All Child Pauses
lia-librechat Oct 23, 2026
d181a80
fix: Discover All Pending Nested Checkpoint Interrupts
lia-librechat Oct 23, 2026
124d8b2
test: Type Mixed-Depth Interrupt Resume Values
lia-librechat Oct 23, 2026
f3c72a1
fix: Unify Command Resume and Preserve Opaque Interrupt Payloads
lia-librechat Oct 23, 2026
9400cbc
fix: Isolate Root Resume Manifests and Private Replay State
lia-librechat Oct 23, 2026
dbafd0c
fix: Reject Unmatched Pending Approval Owners
lia-librechat Oct 23, 2026
c82260e
fix: Preserve Unaddressed Checkpoint Approval Barriers
lia-librechat Oct 23, 2026
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
3 changes: 3 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/** Host context preparation and trusted subagent execution identity are supported. */
export const SUBAGENT_CONTEXT_VERSION = 1;

/** Foreground graph-subagent approval and durable resume support. */
export const GRAPH_SUBAGENT_HITL_VERSION = 1;

/**
* Anthropic direct API tool schema overhead multiplier.
* Empirically calibrated against real MCP tool sets (29 tools).
Expand Down
60 changes: 58 additions & 2 deletions src/graphs/__tests__/Graph.subagentResumeState.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { describe, it, expect } from '@jest/globals';
import type { SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
import { StandardGraph } from '../Graph';
import { Providers } from '@/common';
import { Constants, Providers } from '@/common';

type ResumeToolNode = {
createSubagentResumeState(): SubagentToolNodeResumeState;
restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
};

const makeGraph = (runId: string): StandardGraph =>
const makeGraph = (runId: string, codeSessionKey?: string): StandardGraph =>
new StandardGraph({
runId,
agents: [
{
agentId: 'child-agent',
codeSessionKey,
provider: Providers.OPENAI,
instructions: 'Test child state restoration.',
},
Expand Down Expand Up @@ -77,4 +78,59 @@ describe('subagent graph resume state', () => {
new Set(['unstable_search'])
);
});

it('keeps the legacy default partition identity', () => {
const implicit = initializeToolNode(makeGraph('implicit'));
const explicit = initializeToolNode(
makeGraph('explicit', Constants.EXECUTE_CODE)
);
expect(explicit.createSubagentResumeState().stateKey).toBe(
implicit.createSubagentResumeState().stateKey
);
});

it.each([undefined, 'partition-B'])(
'rejects partition-A restoration into %s before replacing state',
(codeSessionKey) => {
const source = makeGraph('source', 'partition-A');
initializeToolNode(source);
source.sessions.set('partition-A', {
session_id: 'session-A',
lastUpdated: 1,
});
const target = makeGraph('target', codeSessionKey);
initializeToolNode(target);
target.sessions.set('sentinel', {
session_id: 'unchanged',
lastUpdated: 2,
});
target.toolCallStepIds.set('existing', 'unchanged-step');
const before = target.createSubagentResumeState('target-scope');
expect(() =>
target.restoreSubagentResumeState(
source.createSubagentResumeState('source-scope'),
'target-scope'
)
).toThrow('Cannot restore subagent tool state');
expect(target.createSubagentResumeState('target-scope')).toEqual(before);
}
);

it('restores an unchanged custom partition', () => {
const source = makeGraph('source', 'partition-A');
initializeToolNode(source);
source.sessions.set('partition-A', {
session_id: 'session-A',
lastUpdated: 1,
});
const target = makeGraph('target', 'partition-A');
initializeToolNode(target);
target.restoreSubagentResumeState(
source.createSubagentResumeState('source-scope'),
'target-scope'
);
expect(target.sessions.get('partition-A')).toEqual(
source.sessions.get('partition-A')
);
});
});
27 changes: 26 additions & 1 deletion src/hitl/approvalReview.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { createToolApprovalReviewEvidence } from './approvalReview';
import { createToolApprovalReviewEvidence, cloneToolApprovalInterruptPayload } from './approvalReview';

describe('approval review evidence', () => {
const request = {
Expand Down Expand Up @@ -102,4 +102,29 @@ describe('approval review evidence', () => {
).toBeUndefined();
}
);

it.each([
{ resolve: () => 'resolved' },
{ resolution: Symbol('resolved') },
new Date(0),
])('detaches approval fields without cloning opaque checkpoint siblings: %p', (opaque) => {
const manifest = { pendingInterrupts: [{ interruptId: 'sibling', payload: opaque }] };
const payload = {
type: 'tool_approval' as const,
action_requests: [{ ...request, arguments: { command: 'reviewed', options: { cwd: '/safe' } } }],
review_configs: [{ ...reviewConfig, allowed_decisions: ['approve', 'reject'] }],
subagent: { run_id: 'child', agent_id: 'member', subagent_type: 'team' },
__librechat_subagent_resume_manifest: manifest,
};
const cloned = cloneToolApprovalInterruptPayload(payload);
expect(cloned.__librechat_subagent_resume_manifest).toBe(manifest);
expect(cloned.__librechat_subagent_resume_manifest.pendingInterrupts[0].payload).toBe(opaque);
cloned.action_requests[0].arguments.options.cwd = '/unsafe';
cloned.review_configs[0].allowed_decisions.length = 0;
cloned.subagent.agent_id = 'foreign';
expect(payload.action_requests[0].arguments.options.cwd).toBe('/safe');
expect(payload.review_configs[0].allowed_decisions).toEqual(['approve', 'reject']);
expect(payload.subagent.agent_id).toBe('member');
});

});
12 changes: 10 additions & 2 deletions src/hitl/approvalReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,17 @@ export interface ReviewedToolApproval {

const APPROVAL_DECISIONS = new Set(['approve', 'reject', 'edit', 'respond']);

/** Detach approval payloads from host- or transport-owned object graphs. */
/** Detach approval fields without deep-cloning checkpoint-owned metadata. */
export function cloneToolApprovalInterruptPayload<T>(payload: T): T {
return isToolApprovalInterrupt(payload) ? structuredClone(payload) : payload;
if (!isToolApprovalInterrupt(payload)) return payload;
return {
...payload,
...structuredClone({
action_requests: payload.action_requests,
review_configs: payload.review_configs,
...(payload.subagent == null ? {} : { subagent: payload.subagent }),
}),
};
}

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
Loading
Loading