@animalabs/agent-framework 0.7.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/code-execution/index.d.ts +14 -0
- package/dist/src/code-execution/index.d.ts.map +1 -0
- package/dist/src/code-execution/index.js +13 -0
- package/dist/src/code-execution/index.js.map +1 -0
- package/dist/src/code-execution/py-runner.d.ts +96 -0
- package/dist/src/code-execution/py-runner.d.ts.map +1 -0
- package/dist/src/code-execution/py-runner.js +371 -0
- package/dist/src/code-execution/py-runner.js.map +1 -0
- package/dist/src/code-execution/runtime-py.d.ts +32 -0
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -0
- package/dist/src/code-execution/runtime-py.js +265 -0
- package/dist/src/code-execution/runtime-py.js.map +1 -0
- package/dist/src/code-execution/tool-definition.d.ts +16 -0
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -0
- package/dist/src/code-execution/tool-definition.js +47 -0
- package/dist/src/code-execution/tool-definition.js.map +1 -0
- package/dist/src/framework.d.ts +43 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +274 -26
- package/dist/src/framework.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +12 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +45 -0
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +24 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -0
- package/dist/src/mcpl/prose-grammar.js +33 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -0
- package/dist/src/mcpl/prose-stream-router.d.ts +58 -0
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -0
- package/dist/src/mcpl/prose-stream-router.js +168 -0
- package/dist/src/mcpl/prose-stream-router.js.map +1 -0
- package/dist/src/types/framework.d.ts +33 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/test/code-execution.test.d.ts +11 -0
- package/dist/test/code-execution.test.d.ts.map +1 -0
- package/dist/test/code-execution.test.js +423 -0
- package/dist/test/code-execution.test.js.map +1 -0
- package/dist/test/prose-stream-router.test.d.ts +2 -0
- package/dist/test/prose-stream-router.test.d.ts.map +1 -0
- package/dist/test/prose-stream-router.test.js +87 -0
- package/dist/test/prose-stream-router.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/code-execution/index.ts +19 -0
- package/src/code-execution/py-runner.ts +439 -0
- package/src/code-execution/runtime-py.ts +265 -0
- package/src/code-execution/tool-definition.ts +54 -0
- package/src/framework.ts +307 -28
- package/src/index.ts +17 -0
- package/src/mcpl/channel-registry.ts +53 -0
- package/src/mcpl/prose-grammar.ts +40 -0
- package/src/mcpl/prose-stream-router.ts +161 -0
- package/src/types/framework.ts +35 -0
package/dist/src/framework.js
CHANGED
|
@@ -11,11 +11,16 @@ import { FeatureSetManager } from './mcpl/feature-set-manager.js';
|
|
|
11
11
|
import { ScopeManager } from './mcpl/scope-manager.js';
|
|
12
12
|
import { HookOrchestrator } from './mcpl/hook-orchestrator.js';
|
|
13
13
|
import { PushHandler } from './mcpl/push-handler.js';
|
|
14
|
+
import { parseProsePrefix } from './mcpl/prose-grammar.js';
|
|
15
|
+
import { ProseStreamRouter } from './mcpl/prose-stream-router.js';
|
|
14
16
|
import { InferenceRouter } from './mcpl/inference-router.js';
|
|
15
17
|
import { ChannelRegistry } from './mcpl/channel-registry.js';
|
|
16
18
|
import { ConversationRouter } from './mcpl/conversation-router.js';
|
|
17
19
|
import { safeSlice } from './safe-slice.js';
|
|
18
20
|
import { toolResultDataToHistoryString } from './tool-result-history.js';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
import { PyRunner, buildInjectedTools } from './code-execution/py-runner.js';
|
|
23
|
+
import { buildCodeExecutionToolDefinition, CODE_EXECUTION_TOOL_NAME, } from './code-execution/tool-definition.js';
|
|
19
24
|
import { splitProseSegments } from './prose-segments.js';
|
|
20
25
|
/** Detect a supported image media type from magic bytes (the model API
|
|
21
26
|
* rejects mislabeled media types, so trust bytes over extensions).
|
|
@@ -110,29 +115,9 @@ function extractChannelIdFromToolResult(content) {
|
|
|
110
115
|
* when picking a turn's frozen speech locus.
|
|
111
116
|
*/
|
|
112
117
|
const isAddressedMessage = (tags, _metadata) => Array.isArray(tags) && tags.includes('chat:addressed');
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* `>>target [!] [body…]` — target = channel spec or `private`
|
|
117
|
-
* The ` !` continuation modifier must immediately follow the target,
|
|
118
|
-
* whitespace-separated. Body may start on the same line or the next.
|
|
119
|
-
*/
|
|
120
|
-
function parseProsePrefix(text) {
|
|
121
|
-
const m = /^>>(\S+)([ \t]+!)?[ \t]*\n?/.exec(text);
|
|
122
|
-
if (!m)
|
|
123
|
-
return { kind: 'none', continueTurn: false, body: text };
|
|
124
|
-
const target = m[1];
|
|
125
|
-
const continueTurn = m[2] !== undefined;
|
|
126
|
-
const body = text.slice(m[0].length);
|
|
127
|
-
// `skip_reply` mirrors the tool the model already knows: text stays in
|
|
128
|
-
// context, nothing is delivered. (Deliberately NOT called "private"/"note" —
|
|
129
|
-
// that vocabulary sat adjacent to signed thinking in a live window and drew
|
|
130
|
-
// a reasoning_extraction classifier hit, 2026-07-24 Fable.)
|
|
131
|
-
if (target === 'skip_reply') {
|
|
132
|
-
return { kind: 'private', continueTurn, body };
|
|
133
|
-
}
|
|
134
|
-
return { kind: 'target', target, continueTurn, body };
|
|
135
|
-
}
|
|
118
|
+
// parseProsePrefix moved to ./mcpl/prose-grammar.ts — ONE grammar shared with
|
|
119
|
+
// the outgoing-stream router (Spec 14.3), so streamed chunks and delivered
|
|
120
|
+
// envelopes can never disagree on what is a prefix.
|
|
136
121
|
/** One-time primer appended when an agent's proseRouting mode changes. */
|
|
137
122
|
function proseModePrimer(mode) {
|
|
138
123
|
if (mode === 'locus') {
|
|
@@ -442,6 +427,17 @@ export class AgentFramework {
|
|
|
442
427
|
/** Forks whose TTL closure turn has been queued — disposed (removed from
|
|
443
428
|
* agents/agentConfigs/conversationAgentHomes) when their stream ends. */
|
|
444
429
|
closingConversationAgents = new Set();
|
|
430
|
+
// Client-side programmatic tool calling (`code_execution`). Null unless
|
|
431
|
+
// config.codeExecution.enabled. One PyRunner per agent (interpreter state
|
|
432
|
+
// is per-agent, like a per-agent container); waiters resolve script-inner
|
|
433
|
+
// tool calls dispatched through the normal dispatchToolCall path.
|
|
434
|
+
codeExecutionConfig = null;
|
|
435
|
+
codeExecutionRunners = new Map();
|
|
436
|
+
scriptToolWaiters = new Map();
|
|
437
|
+
/** Agents whose running script hit an endTurn-carrying inner result —
|
|
438
|
+
* deferred and applied to the final code_execution result instead of
|
|
439
|
+
* cancelling the stream mid-script (which would wedge the turn). */
|
|
440
|
+
scriptDeferredEndTurn = new Set();
|
|
445
441
|
mcplTools = [];
|
|
446
442
|
mcplToolRefreshInFlight = false;
|
|
447
443
|
mcplToolRefreshPending = false;
|
|
@@ -659,6 +655,9 @@ export class AgentFramework {
|
|
|
659
655
|
// Stored for lazy MCPL initialization (connectMcplServer on a framework
|
|
660
656
|
// that started with zero configured servers).
|
|
661
657
|
framework.mcplInferenceRoutingConfig = config.inferenceRouting ?? null;
|
|
658
|
+
// Client-side programmatic tool calling (code_execution). Config is only
|
|
659
|
+
// retained when enabled — everything downstream gates on the field.
|
|
660
|
+
framework.codeExecutionConfig = config.codeExecution?.enabled ? config.codeExecution : null;
|
|
662
661
|
// Initialize MCPL subsystems if configured
|
|
663
662
|
if (config.mcplServers && config.mcplServers.length > 0) {
|
|
664
663
|
// Validate tool prefixes: no collisions with module names or between servers
|
|
@@ -739,6 +738,13 @@ export class AgentFramework {
|
|
|
739
738
|
async stop() {
|
|
740
739
|
this.running = false;
|
|
741
740
|
this.queue.close();
|
|
741
|
+
// Kill running code_execution scripts before cancelling streams: a
|
|
742
|
+
// zombie script must not keep firing side-effectful tool calls into a
|
|
743
|
+
// framework that is shutting down.
|
|
744
|
+
for (const runner of this.codeExecutionRunners.values()) {
|
|
745
|
+
runner.dispose();
|
|
746
|
+
}
|
|
747
|
+
this.codeExecutionRunners.clear();
|
|
742
748
|
// Cancel all active streams
|
|
743
749
|
for (const agent of this.agents.values()) {
|
|
744
750
|
if (agent.state.status === 'streaming' ||
|
|
@@ -1004,6 +1010,9 @@ export class AgentFramework {
|
|
|
1004
1010
|
...(this.getWorkspaceModule()
|
|
1005
1011
|
? [AgentFramework.SAVE_IMAGE_TOOL, AgentFramework.READ_IMAGE_TOOL]
|
|
1006
1012
|
: []),
|
|
1013
|
+
...(this.codeExecutionConfig
|
|
1014
|
+
? [buildCodeExecutionToolDefinition(this.codeExecutionConfig)]
|
|
1015
|
+
: []),
|
|
1007
1016
|
];
|
|
1008
1017
|
}
|
|
1009
1018
|
/** The registered workspace module, if any (used by save_image). */
|
|
@@ -1274,7 +1283,7 @@ export class AgentFramework {
|
|
|
1274
1283
|
*
|
|
1275
1284
|
* Returns null when the agent's strategy has no fold plan (non-adaptive).
|
|
1276
1285
|
*/
|
|
1277
|
-
previewContextSettings(agentName, budgetTokens, overrides) {
|
|
1286
|
+
previewContextSettings(agentName, budgetTokens, overrides, opts) {
|
|
1278
1287
|
const agent = this.agents.get(agentName);
|
|
1279
1288
|
if (!agent)
|
|
1280
1289
|
throw new Error(`Unknown agent: ${agentName}`);
|
|
@@ -1282,8 +1291,10 @@ export class AgentFramework {
|
|
|
1282
1291
|
if (typeof cm.previewContext !== 'function')
|
|
1283
1292
|
return null;
|
|
1284
1293
|
// reserveForResponse mirrors the live compile so the previewed middle is
|
|
1285
|
-
// comparable to what the agent actually gets.
|
|
1286
|
-
|
|
1294
|
+
// comparable to what the agent actually gets. `opts.render` additionally
|
|
1295
|
+
// returns the rendered entries — opt-in, since they are megabytes on a
|
|
1296
|
+
// large store, and older context-manager builds ignore the argument.
|
|
1297
|
+
return cm.previewContext({ maxTokens: budgetTokens, reserveForResponse: agent.maxTokens }, overrides, opts);
|
|
1287
1298
|
}
|
|
1288
1299
|
/**
|
|
1289
1300
|
* Check if process logging is enabled.
|
|
@@ -2785,6 +2796,18 @@ export class AgentFramework {
|
|
|
2785
2796
|
// Anthropic API. If we let modules or MCPL handlers run first they may add
|
|
2786
2797
|
// messages between tool_use and tool_result, causing a 400 error.
|
|
2787
2798
|
if (event.type === 'tool-result') {
|
|
2799
|
+
// Script-inner tool calls (client-side programmatic tool calling):
|
|
2800
|
+
// resolve the waiter and stop. These calls belong to a RUNNING
|
|
2801
|
+
// code_execution script, not to the agent's pending tool round — they
|
|
2802
|
+
// must not touch agent state (and must resolve even for ephemeral
|
|
2803
|
+
// agents that aren't in this.agents, or after the agent's own round
|
|
2804
|
+
// has already settled).
|
|
2805
|
+
const scriptWaiter = this.scriptToolWaiters.get(event.callId);
|
|
2806
|
+
if (scriptWaiter) {
|
|
2807
|
+
this.scriptToolWaiters.delete(event.callId);
|
|
2808
|
+
scriptWaiter(event.result);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2788
2811
|
const agent = this.agents.get(event.agentName);
|
|
2789
2812
|
if (agent) {
|
|
2790
2813
|
this.touchEphemeralRun(event.agentName, true);
|
|
@@ -4029,6 +4052,29 @@ export class AgentFramework {
|
|
|
4029
4052
|
: resolveTurnLocus();
|
|
4030
4053
|
if (typingChannel)
|
|
4031
4054
|
this.channelRegistry.startTyping(typingChannel);
|
|
4055
|
+
// MCPL Spec 14.3 outgoing streaming: route text deltas to their
|
|
4056
|
+
// destination server AS THEY GENERATE (voice synthesis, live message
|
|
4057
|
+
// rendering). Pure observer surface — suppression is fail-closed (nothing
|
|
4058
|
+
// streams that delivery would refuse), emission is capability-gated in
|
|
4059
|
+
// the registry (`channels.streaming`), and delivery via deliverProse
|
|
4060
|
+
// remains the sole authoritative send.
|
|
4061
|
+
const outgoingInferenceId = `inf_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
4062
|
+
let outgoingIndex = 0;
|
|
4063
|
+
const proseStream = this.channelRegistry
|
|
4064
|
+
? new ProseStreamRouter({
|
|
4065
|
+
mode: agent.proseRouting === 'explicit' ? 'explicit' : 'locus',
|
|
4066
|
+
initialTarget: typingChannel,
|
|
4067
|
+
resolve: (spec) => {
|
|
4068
|
+
const r = this.channelRegistry.resolveProseTarget(spec);
|
|
4069
|
+
return 'channelId' in r ? r.channelId : null;
|
|
4070
|
+
},
|
|
4071
|
+
})
|
|
4072
|
+
: null;
|
|
4073
|
+
const emitOutgoing = (deltas) => {
|
|
4074
|
+
for (const rd of deltas) {
|
|
4075
|
+
this.channelRegistry.sendOutgoingChunk(rd.channelId, agent.name, outgoingInferenceId, outgoingIndex++, rd.delta);
|
|
4076
|
+
}
|
|
4077
|
+
};
|
|
4032
4078
|
const adoptInjectedRound = () => {
|
|
4033
4079
|
if (!this.midTurnInputSignals.has(agent.name))
|
|
4034
4080
|
return;
|
|
@@ -4054,6 +4100,9 @@ export class AgentFramework {
|
|
|
4054
4100
|
// turn's prose is bound for, without re-deriving routing.
|
|
4055
4101
|
channelId: typingChannel ?? undefined,
|
|
4056
4102
|
});
|
|
4103
|
+
if (proseStream && event.meta.type === 'text') {
|
|
4104
|
+
emitOutgoing(proseStream.feed(event.content));
|
|
4105
|
+
}
|
|
4057
4106
|
break;
|
|
4058
4107
|
case 'block': {
|
|
4059
4108
|
const { event: phase, index, block } = event.event;
|
|
@@ -4570,6 +4619,7 @@ export class AgentFramework {
|
|
|
4570
4619
|
// Only reset + retry if this is still the active stream
|
|
4571
4620
|
if (agent.streamId !== myStreamId)
|
|
4572
4621
|
break;
|
|
4622
|
+
this.abortAgentScript(agent.name, 'stream error');
|
|
4573
4623
|
agent.reset();
|
|
4574
4624
|
const action = this.errorPolicy.onInferenceError(err, agent.name, attempt);
|
|
4575
4625
|
if (action.retry) {
|
|
@@ -4616,6 +4666,7 @@ export class AgentFramework {
|
|
|
4616
4666
|
// may have already started a new stream, bumping streamId)
|
|
4617
4667
|
if (agent.streamId === myStreamId) {
|
|
4618
4668
|
const durationMs = Date.now() - startTime;
|
|
4669
|
+
this.abortAgentScript(agent.name, `stream aborted (${reason})`);
|
|
4619
4670
|
agent.reset();
|
|
4620
4671
|
this.settleAgent(agent.name, {
|
|
4621
4672
|
stopReason: 'exhausted',
|
|
@@ -4714,6 +4765,7 @@ export class AgentFramework {
|
|
|
4714
4765
|
request: compiledRequest ?? { note: 'streaming request threw' },
|
|
4715
4766
|
durationMs,
|
|
4716
4767
|
});
|
|
4768
|
+
this.abortAgentScript(agent.name, 'stream threw');
|
|
4717
4769
|
agent.reset();
|
|
4718
4770
|
this.eventGate?.onInferenceEnded(agent.name);
|
|
4719
4771
|
}
|
|
@@ -4730,6 +4782,15 @@ export class AgentFramework {
|
|
|
4730
4782
|
// Stop the typing indicator on every exit path (complete, error,
|
|
4731
4783
|
// exhausted, abort) so it never sticks after the turn ends.
|
|
4732
4784
|
this.channelRegistry?.stopTyping();
|
|
4785
|
+
// Spec 14.3: flush any held line-start text, then close each streamed
|
|
4786
|
+
// channel with its final moderated content — the consumer's signal to
|
|
4787
|
+
// finalize (end the TTS utterance, settle the rendered message).
|
|
4788
|
+
if (proseStream) {
|
|
4789
|
+
emitOutgoing(proseStream.finish());
|
|
4790
|
+
for (const [channelId, text] of proseStream.byChannel()) {
|
|
4791
|
+
this.channelRegistry.sendOutgoingComplete(channelId, agent.name, outgoingInferenceId, text);
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4733
4794
|
this.frameworkCancelledStreams.delete(`${agent.name}:${myStreamId}`);
|
|
4734
4795
|
this.activeStreams.delete(agent.name);
|
|
4735
4796
|
this.pendingAssistantBlocks.delete(agent.name);
|
|
@@ -4753,6 +4814,12 @@ export class AgentFramework {
|
|
|
4753
4814
|
* Used by SubagentModule to dispatch tool calls for ephemeral agents.
|
|
4754
4815
|
*/
|
|
4755
4816
|
async executeToolCall(call) {
|
|
4817
|
+
// Client-side programmatic tool calling for promise-based callers
|
|
4818
|
+
// (SubagentModule ephemerals). Keyed by callerAgentName so each ephemeral
|
|
4819
|
+
// gets its own interpreter state.
|
|
4820
|
+
if (call.name === CODE_EXECUTION_TOOL_NAME && this.codeExecutionConfig) {
|
|
4821
|
+
return this.runCodeExecution(call.callerAgentName ?? '__ephemeral__', call);
|
|
4822
|
+
}
|
|
4756
4823
|
// MCPL tools are dispatched via the MCPL subsystem
|
|
4757
4824
|
if (this.mcplServerRegistry) {
|
|
4758
4825
|
// Check if this is an MCPL-prefixed tool
|
|
@@ -4800,6 +4867,180 @@ export class AgentFramework {
|
|
|
4800
4867
|
};
|
|
4801
4868
|
}
|
|
4802
4869
|
}
|
|
4870
|
+
// ---------------------------------------------------------------------------
|
|
4871
|
+
// Client-side programmatic tool calling (code_execution)
|
|
4872
|
+
// ---------------------------------------------------------------------------
|
|
4873
|
+
/** Event-pushing wrapper for the model-driven dispatch path. */
|
|
4874
|
+
dispatchCodeExecutionToolCall(agentName, call) {
|
|
4875
|
+
this.emitTrace({
|
|
4876
|
+
type: 'tool:started',
|
|
4877
|
+
module: 'code_execution',
|
|
4878
|
+
tool: call.name,
|
|
4879
|
+
callId: call.id,
|
|
4880
|
+
input: call.input,
|
|
4881
|
+
});
|
|
4882
|
+
const startTime = Date.now();
|
|
4883
|
+
this.runCodeExecution(agentName, call)
|
|
4884
|
+
.catch((error) => {
|
|
4885
|
+
// runCodeExecution is designed not to reject; this is the last-resort
|
|
4886
|
+
// guard so a bug here can never strand the agent in waiting_for_tools.
|
|
4887
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
4888
|
+
return { success: false, error: `code_execution failed: ${err.message}`, isError: true };
|
|
4889
|
+
})
|
|
4890
|
+
.then((result) => {
|
|
4891
|
+
if (result.isError) {
|
|
4892
|
+
this.emitTrace({
|
|
4893
|
+
type: 'tool:failed',
|
|
4894
|
+
module: 'code_execution',
|
|
4895
|
+
tool: call.name,
|
|
4896
|
+
callId: call.id,
|
|
4897
|
+
error: result.error ?? 'unknown error',
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
else {
|
|
4901
|
+
this.emitTrace({
|
|
4902
|
+
type: 'tool:completed',
|
|
4903
|
+
module: 'code_execution',
|
|
4904
|
+
tool: call.name,
|
|
4905
|
+
callId: call.id,
|
|
4906
|
+
durationMs: Date.now() - startTime,
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
this.pushEvent({
|
|
4910
|
+
type: 'tool-result',
|
|
4911
|
+
callId: call.id,
|
|
4912
|
+
agentName,
|
|
4913
|
+
moduleName: 'code_execution',
|
|
4914
|
+
result,
|
|
4915
|
+
});
|
|
4916
|
+
});
|
|
4917
|
+
}
|
|
4918
|
+
/**
|
|
4919
|
+
* Run one code_execution script for an agent. Never rejects.
|
|
4920
|
+
*
|
|
4921
|
+
* The script runs in the agent's persistent python interpreter with every
|
|
4922
|
+
* tool on the agent's CURRENT surface injected (minus code_execution
|
|
4923
|
+
* itself). Inner tool calls are dispatched through dispatchToolCall — the
|
|
4924
|
+
* exact model path, including MCPL state/checkpoint handling, channel
|
|
4925
|
+
* routing, and traces — and resolved via scriptToolWaiters, so their
|
|
4926
|
+
* results reach the running script and never the model context.
|
|
4927
|
+
*/
|
|
4928
|
+
async runCodeExecution(agentName, call) {
|
|
4929
|
+
const input = (call.input ?? {});
|
|
4930
|
+
if (typeof input.code !== 'string' || input.code.trim() === '') {
|
|
4931
|
+
return {
|
|
4932
|
+
success: false,
|
|
4933
|
+
error: 'code_execution requires a non-empty `code` string',
|
|
4934
|
+
isError: true,
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4937
|
+
const agent = this.agents.get(agentName);
|
|
4938
|
+
const surface = agent
|
|
4939
|
+
? this.getToolsForAgent(agentName).filter((t) => agent.canUseTool(t.name))
|
|
4940
|
+
: this.getAllTools(); // ephemeral agents: full surface, matching executeToolCall
|
|
4941
|
+
const injected = buildInjectedTools(surface.map((t) => t.name).filter((name) => name !== CODE_EXECUTION_TOOL_NAME));
|
|
4942
|
+
const runner = this.getOrCreateScriptRunner(agentName);
|
|
4943
|
+
this.scriptDeferredEndTurn.delete(agentName);
|
|
4944
|
+
const exec = await runner.exec(input.code, injected);
|
|
4945
|
+
const endTurn = this.scriptDeferredEndTurn.delete(agentName);
|
|
4946
|
+
return {
|
|
4947
|
+
success: true,
|
|
4948
|
+
data: { stdout: exec.stdout, stderr: exec.stderr, return_code: exec.returnCode },
|
|
4949
|
+
isError: false,
|
|
4950
|
+
...(endTurn ? { endTurn: true } : {}),
|
|
4951
|
+
};
|
|
4952
|
+
}
|
|
4953
|
+
getOrCreateScriptRunner(agentName) {
|
|
4954
|
+
let runner = this.codeExecutionRunners.get(agentName);
|
|
4955
|
+
if (!runner) {
|
|
4956
|
+
const cfg = this.codeExecutionConfig;
|
|
4957
|
+
runner = new PyRunner({
|
|
4958
|
+
pythonPath: cfg?.pythonPath,
|
|
4959
|
+
toolCallTimeoutMs: cfg?.toolCallTimeoutMs,
|
|
4960
|
+
scriptTimeoutMs: cfg?.scriptTimeoutMs,
|
|
4961
|
+
idleReclaimMs: cfg?.idleReclaimMs,
|
|
4962
|
+
label: agentName,
|
|
4963
|
+
onToolCall: (toolName, args) => this.handleScriptToolCall(agentName, toolName, args),
|
|
4964
|
+
});
|
|
4965
|
+
this.codeExecutionRunners.set(agentName, runner);
|
|
4966
|
+
}
|
|
4967
|
+
return runner;
|
|
4968
|
+
}
|
|
4969
|
+
/**
|
|
4970
|
+
* Resolve one script-inner tool call to the string the script's tool
|
|
4971
|
+
* function returns. Never rejects — errors become "Error: ..." strings,
|
|
4972
|
+
* matching the managed PTC runtime ("Claude's code receives this error").
|
|
4973
|
+
*
|
|
4974
|
+
* Results are serialized with the HISTORY serializer (images become
|
|
4975
|
+
* placeholders): programmatic tool results are text-only by contract, and
|
|
4976
|
+
* a script variable holding megabytes of base64 helps nobody.
|
|
4977
|
+
*/
|
|
4978
|
+
async handleScriptToolCall(agentName, toolName, args) {
|
|
4979
|
+
if (toolName === CODE_EXECUTION_TOOL_NAME) {
|
|
4980
|
+
return 'Error: code_execution cannot be called from within a script';
|
|
4981
|
+
}
|
|
4982
|
+
const result = await this.dispatchScriptToolCall(agentName, toolName, args);
|
|
4983
|
+
if (result.endTurn) {
|
|
4984
|
+
// Deferred: applied to the final code_execution result (see
|
|
4985
|
+
// scriptDeferredEndTurn) — ending the turn mid-script would cancel the
|
|
4986
|
+
// stream while the script still runs and wedge the tool round.
|
|
4987
|
+
this.scriptDeferredEndTurn.add(agentName);
|
|
4988
|
+
}
|
|
4989
|
+
if (result.isError) {
|
|
4990
|
+
return `Error: ${result.error ?? 'tool call failed'}`;
|
|
4991
|
+
}
|
|
4992
|
+
if (result.data === undefined)
|
|
4993
|
+
return '';
|
|
4994
|
+
const agent = this.agents.get(agentName);
|
|
4995
|
+
const maxChars = agent ? this.getMaxToolResultChars(agent) : undefined;
|
|
4996
|
+
return toolResultDataToHistoryString(result.data, maxChars);
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4999
|
+
* Dispatch a script-inner tool call through the NORMAL dispatch path and
|
|
5000
|
+
* resolve with its result. The waiter intercept in handleProcessEvent
|
|
5001
|
+
* (keyed by the pytc- call ID) routes the tool-result event here instead
|
|
5002
|
+
* of into the agent's pending tool round.
|
|
5003
|
+
*/
|
|
5004
|
+
dispatchScriptToolCall(agentName, toolName, args) {
|
|
5005
|
+
return new Promise((resolve) => {
|
|
5006
|
+
const callId = `pytc-${randomUUID()}`;
|
|
5007
|
+
// Host-side leak guard: the python runtime already raises TimeoutError
|
|
5008
|
+
// at toolCallTimeoutMs; this only reclaims the waiter if a dispatch path
|
|
5009
|
+
// loses the result entirely (so the map cannot grow unboundedly).
|
|
5010
|
+
const safetyMs = (this.codeExecutionConfig?.toolCallTimeoutMs ?? 270_000) + 30_000;
|
|
5011
|
+
const safety = setTimeout(() => {
|
|
5012
|
+
if (this.scriptToolWaiters.delete(callId)) {
|
|
5013
|
+
resolve({
|
|
5014
|
+
success: false,
|
|
5015
|
+
error: `tool '${toolName}' produced no result within ${Math.round(safetyMs / 1000)}s`,
|
|
5016
|
+
isError: true,
|
|
5017
|
+
});
|
|
5018
|
+
}
|
|
5019
|
+
}, safetyMs);
|
|
5020
|
+
safety.unref?.();
|
|
5021
|
+
this.scriptToolWaiters.set(callId, (result) => {
|
|
5022
|
+
clearTimeout(safety);
|
|
5023
|
+
resolve(result);
|
|
5024
|
+
});
|
|
5025
|
+
try {
|
|
5026
|
+
this.dispatchToolCall(agentName, { id: callId, name: toolName, input: args });
|
|
5027
|
+
}
|
|
5028
|
+
catch (error) {
|
|
5029
|
+
if (this.scriptToolWaiters.delete(callId)) {
|
|
5030
|
+
clearTimeout(safety);
|
|
5031
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
5032
|
+
resolve({ success: false, error: err.message, isError: true });
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
});
|
|
5036
|
+
}
|
|
5037
|
+
/** Abort a running script when the agent's turn dies underneath it. */
|
|
5038
|
+
abortAgentScript(agentName, reason) {
|
|
5039
|
+
const runner = this.codeExecutionRunners.get(agentName);
|
|
5040
|
+
if (runner?.busy) {
|
|
5041
|
+
runner.abort(reason);
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
4803
5044
|
toMembraneToolResult(callId, afResult, maxChars) {
|
|
4804
5045
|
if (afResult.isError) {
|
|
4805
5046
|
return { toolUseId: callId, content: afResult.error ?? 'Unknown error', isError: true };
|
|
@@ -5018,6 +5259,13 @@ export class AgentFramework {
|
|
|
5018
5259
|
this.dispatchChannelToolCall(agentName, enrichedCall);
|
|
5019
5260
|
return;
|
|
5020
5261
|
}
|
|
5262
|
+
// Client-side programmatic tool calling: run a python script that calls
|
|
5263
|
+
// the agent's other tools in-process (script-inner calls come back through
|
|
5264
|
+
// this very dispatcher with waiter-tracked call IDs).
|
|
5265
|
+
if (enrichedCall.name === CODE_EXECUTION_TOOL_NAME && this.codeExecutionConfig) {
|
|
5266
|
+
this.dispatchCodeExecutionToolCall(agentName, enrichedCall);
|
|
5267
|
+
return;
|
|
5268
|
+
}
|
|
5021
5269
|
// prose_help: on-demand routing reference for explicit-prose agents.
|
|
5022
5270
|
// Answered inline — the grammar is a tool RESULT (model-requested), never
|
|
5023
5271
|
// ambient context (see PROSE_ROUTING_HELP / teach-by-bounce).
|