@animalabs/agent-framework 0.21.1 → 0.22.0
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/README.md +43 -0
- package/dist/src/agent.d.ts +17 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +102 -42
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +22 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +64 -9
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +4 -0
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +215 -38
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/framework.d.ts +250 -39
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1602 -371
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +21 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +40 -7
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +5 -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.map +1 -1
- package/dist/src/mcpl/channel-registry.js +26 -3
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +28 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +17 -0
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +31 -7
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +1 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +85 -22
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +32 -4
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +46 -13
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +5 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +41 -11
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +4 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +45 -8
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +5 -2
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +25 -7
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +47 -1
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
- package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
- package/dist/src/mcpl/tool-pattern-check.js +79 -0
- package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +15 -5
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +39 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -0
- package/dist/src/mcpl/visible-content.js +54 -0
- package/dist/src/mcpl/visible-content.js.map +1 -0
- package/dist/src/module-registry.d.ts +81 -3
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +257 -16
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
- package/dist/src/modules/workspace/branch-intent.js +83 -0
- package/dist/src/modules/workspace/branch-intent.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.js +259 -0
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +84 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -0
- package/dist/src/modules/workspace/effects.js +368 -0
- package/dist/src/modules/workspace/effects.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +155 -17
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1059 -424
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +278 -0
- package/dist/src/modules/workspace/observe.d.ts.map +1 -0
- package/dist/src/modules/workspace/observe.js +805 -0
- package/dist/src/modules/workspace/observe.js.map +1 -0
- package/dist/src/modules/workspace/reconcile.d.ts +224 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
- package/dist/src/modules/workspace/reconcile.js +901 -0
- package/dist/src/modules/workspace/reconcile.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +6 -59
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +6 -257
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +77 -10
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/record-journal.d.ts +127 -0
- package/dist/src/record-journal.d.ts.map +1 -0
- package/dist/src/record-journal.js +202 -0
- package/dist/src/record-journal.js.map +1 -0
- package/dist/src/shutdown.d.ts +13 -0
- package/dist/src/shutdown.d.ts.map +1 -0
- package/dist/src/shutdown.js +46 -0
- package/dist/src/shutdown.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +74 -0
- package/dist/src/silent-heartbeat.d.ts.map +1 -0
- package/dist/src/silent-heartbeat.js +77 -0
- package/dist/src/silent-heartbeat.js.map +1 -0
- package/dist/src/tool-presentation.d.ts +50 -0
- package/dist/src/tool-presentation.d.ts.map +1 -0
- package/dist/src/tool-presentation.js +235 -0
- package/dist/src/tool-presentation.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +125 -6
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +310 -35
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +13 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +21 -1
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +18 -2
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +4 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +12 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +14 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +73 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/batched-wake-locus.test.d.ts +2 -0
- package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
- package/dist/test/batched-wake-locus.test.js +166 -0
- package/dist/test/batched-wake-locus.test.js.map +1 -0
- package/dist/test/code-execution.test.js +386 -2
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/component-defaults.test.d.ts +2 -0
- package/dist/test/component-defaults.test.d.ts.map +1 -0
- package/dist/test/component-defaults.test.js +54 -0
- package/dist/test/component-defaults.test.js.map +1 -0
- package/dist/test/conversation-routing.test.js +57 -1
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/ephemeral-settle.test.js +3 -0
- package/dist/test/ephemeral-settle.test.js.map +1 -1
- package/dist/test/event-gate.test.js +118 -0
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +977 -0
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
- package/dist/test/helpers/pytc-stop-rows.js +143 -0
- package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.js +19 -6
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
- package/dist/test/mcpl-config-diagnostics.test.js +449 -0
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
- package/dist/test/mcpl-empty-content.test.d.ts +2 -0
- package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
- package/dist/test/mcpl-empty-content.test.js +368 -0
- package/dist/test/mcpl-empty-content.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
- package/dist/test/prose-stream-router.test.js +41 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/record-journal.test.d.ts +6 -0
- package/dist/test/record-journal.test.d.ts.map +1 -0
- package/dist/test/record-journal.test.js +269 -0
- package/dist/test/record-journal.test.js.map +1 -0
- package/dist/test/save-recent-image.test.js +102 -0
- package/dist/test/save-recent-image.test.js.map +1 -1
- package/dist/test/shutdown-errors.test.d.ts +2 -0
- package/dist/test/shutdown-errors.test.d.ts.map +1 -0
- package/dist/test/shutdown-errors.test.js +662 -0
- package/dist/test/shutdown-errors.test.js.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.js +451 -0
- package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
- package/dist/test/silent-heartbeat.test.js +49 -38
- package/dist/test/silent-heartbeat.test.js.map +1 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
- package/dist/test/stop-with-work-in-flight.test.js +672 -0
- package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
- package/dist/test/tool-batch-hook.test.d.ts +8 -0
- package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
- package/dist/test/tool-batch-hook.test.js +275 -0
- package/dist/test/tool-batch-hook.test.js.map +1 -0
- package/dist/test/tool-presentation.test.d.ts +2 -0
- package/dist/test/tool-presentation.test.d.ts.map +1 -0
- package/dist/test/tool-presentation.test.js +268 -0
- package/dist/test/tool-presentation.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.js +2330 -18
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/tool-result-history.test.js +2 -1
- package/dist/test/tool-result-history.test.js.map +1 -1
- package/dist/test/turn-trigger-provenance.test.js +35 -0
- package/dist/test/turn-trigger-provenance.test.js.map +1 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
- package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
- package/dist/test/unconvertible-tool-result.test.js +358 -0
- package/dist/test/unconvertible-tool-result.test.js.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.js +252 -0
- package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
- package/dist/test/workspace-three-way.test.d.ts +12 -0
- package/dist/test/workspace-three-way.test.d.ts.map +1 -0
- package/dist/test/workspace-three-way.test.js +3079 -0
- package/dist/test/workspace-three-way.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +120 -44
- package/src/code-execution/py-runner.ts +66 -9
- package/src/code-execution/runtime-py.ts +215 -38
- package/src/framework.ts +1661 -363
- package/src/gate/event-gate.ts +59 -7
- package/src/index.ts +11 -0
- package/src/mcpl/channel-registry.ts +24 -3
- package/src/mcpl/conversation-router.ts +34 -1
- package/src/mcpl/feature-set-manager.ts +36 -11
- package/src/mcpl/index.ts +2 -0
- package/src/mcpl/prose-stream-router.ts +79 -23
- package/src/mcpl/push-coalescer.ts +62 -13
- package/src/mcpl/push-handler.ts +41 -12
- package/src/mcpl/server-connection.ts +41 -10
- package/src/mcpl/server-registry.ts +24 -7
- package/src/mcpl/tool-lifecycle.ts +77 -2
- package/src/mcpl/tool-pattern-check.ts +106 -0
- package/src/mcpl/types.ts +15 -5
- package/src/mcpl/visible-content.ts +57 -0
- package/src/module-registry.ts +252 -16
- package/src/modules/workspace/branch-intent.ts +123 -0
- package/src/modules/workspace/disk-agreement.ts +325 -0
- package/src/modules/workspace/effects.ts +362 -0
- package/src/modules/workspace/index.ts +1058 -446
- package/src/modules/workspace/observe.ts +839 -0
- package/src/modules/workspace/reconcile.ts +1136 -0
- package/src/modules/workspace/sync.ts +6 -336
- package/src/modules/workspace/types.ts +75 -10
- package/src/record-journal.ts +246 -0
- package/src/shutdown.ts +49 -0
- package/src/silent-heartbeat.ts +141 -0
- package/src/tool-presentation.ts +188 -0
- package/src/tool-result-guard.ts +335 -33
- package/src/tool-result-history.ts +25 -1
- package/src/types/agent.ts +18 -2
- package/src/types/events.ts +4 -0
- package/src/types/framework.ts +12 -0
- package/src/types/module.ts +15 -0
- package/src/types/trace.ts +80 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/agent-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@animalabs/chronicle": "^0.4.0",
|
|
50
50
|
"@animalabs/context-manager": "^0.14.0",
|
|
51
|
-
"@animalabs/membrane": "^0.5.
|
|
51
|
+
"@animalabs/membrane": "^0.5.87",
|
|
52
52
|
"chokidar": "^4.0.3",
|
|
53
53
|
"discord.js": "^14.25.1",
|
|
54
54
|
"ws": "^8.18.0"
|
package/src/agent.ts
CHANGED
|
@@ -2,13 +2,26 @@ import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, Yiel
|
|
|
2
2
|
import { isAbortedResponse } from '@animalabs/membrane';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import type { CacheWireReceipt, KvUnifiedRequestHooks } from './kv-unified-wire.js';
|
|
5
|
-
import { ToolResultGuard
|
|
5
|
+
import { ToolResultGuard } from './tool-result-guard.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
indexSilentTickRows,
|
|
8
|
+
separateSilentHeartbeatTicks,
|
|
9
|
+
silentHeartbeatSeparatorTurn,
|
|
10
|
+
type SilentHeartbeatTick,
|
|
11
|
+
type SilentTickIndex,
|
|
12
|
+
type StoredRowLike,
|
|
13
|
+
} from './silent-heartbeat.js';
|
|
14
|
+
import {
|
|
15
|
+
nonErrorResultText,
|
|
8
16
|
truncateForHistory,
|
|
9
17
|
DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS,
|
|
10
18
|
} from './tool-result-history.js';
|
|
11
19
|
|
|
20
|
+
export interface ActivationRequestOptions {
|
|
21
|
+
/** The silent heartbeat tick this request answers, if any. */
|
|
22
|
+
silentHeartbeat?: SilentHeartbeatTick;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
export interface StartStreamResult {
|
|
13
26
|
stream: YieldingStream;
|
|
14
27
|
request: NormalizedRequest;
|
|
@@ -22,6 +35,7 @@ import type {
|
|
|
22
35
|
CompileResult,
|
|
23
36
|
HotContextSettingsStatus,
|
|
24
37
|
HotContextSettingsUpdate,
|
|
38
|
+
MessageMetadata,
|
|
25
39
|
} from '@animalabs/context-manager';
|
|
26
40
|
import type {
|
|
27
41
|
AgentConfig,
|
|
@@ -656,28 +670,37 @@ export class Agent {
|
|
|
656
670
|
this.toolResultGuard.storeResults(content, wireResults, this._state.toolResults);
|
|
657
671
|
}
|
|
658
672
|
|
|
659
|
-
// Compile context (with optional injections)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
673
|
+
// Compile the context (with optional injections) into a request. The
|
|
674
|
+
// guard's refusal retry is assembled the same way, afresh. It is compiled
|
|
675
|
+
// only once each withheld stub's annotation has finished, including one
|
|
676
|
+
// that began during the compile, and after a stub edit that failed has
|
|
677
|
+
// been retried; one whose store still fails is compiled as the bare
|
|
678
|
+
// notice.
|
|
679
|
+
let unguardedResults: ReturnType<Agent['buildToolResultMessages']> | undefined;
|
|
680
|
+
const assemble = async (): Promise<NormalizedRequest> => {
|
|
681
|
+
const { messages, systemInjections } = await this.toolResultGuard.afterAnnotations(
|
|
682
|
+
() => this.compileWithInjections(budget, injections));
|
|
683
|
+
// Read once, after the first compile, since results may have become
|
|
684
|
+
// ready while it ran; the refusal retry, assembled while the agent is
|
|
685
|
+
// inferring, sends the same ones.
|
|
686
|
+
unguardedResults ??= this._state.status === 'ready' && !guardedResults
|
|
687
|
+
? this.buildToolResultMessages(this._state.toolResults) : [];
|
|
688
|
+
messages.push(...unguardedResults);
|
|
689
|
+
return {
|
|
690
|
+
messages: this.toolResultGuard.prepareRequest(messages, true),
|
|
691
|
+
system: this.buildSystemPrompt(systemInjections),
|
|
692
|
+
config: {
|
|
693
|
+
model: this.model,
|
|
694
|
+
maxTokens: this.maxTokens,
|
|
695
|
+
...(this.temperature !== undefined && { temperature: this.temperature }),
|
|
696
|
+
...(this.thinking !== undefined && { thinking: this.thinking }),
|
|
697
|
+
},
|
|
698
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
699
|
+
...(this.providerParams && { providerParams: this.providerParams }),
|
|
700
|
+
assistantParticipant: this.name,
|
|
701
|
+
};
|
|
680
702
|
};
|
|
703
|
+
const request = await assemble();
|
|
681
704
|
|
|
682
705
|
const abortController = new AbortController();
|
|
683
706
|
if (options?.signal) {
|
|
@@ -690,7 +713,7 @@ export class Agent {
|
|
|
690
713
|
|
|
691
714
|
// Set state to inferring
|
|
692
715
|
this._inferenceStartedAt = Date.now();
|
|
693
|
-
const inferencePromise = this.doInference(request, abortController.signal);
|
|
716
|
+
const inferencePromise = this.doInference(request, abortController.signal, assemble);
|
|
694
717
|
this._state = { status: 'inferring', promise: inferencePromise, abortController };
|
|
695
718
|
|
|
696
719
|
try {
|
|
@@ -779,20 +802,27 @@ export class Agent {
|
|
|
779
802
|
* it does not touch agent state and does not call the membrane, and
|
|
780
803
|
* `ContextManager.compile` is itself side-effect-free (compression runs in
|
|
781
804
|
* the background). Safe to call regardless of the agent's current status.
|
|
805
|
+
* It doesn't wait for the tool-result guard's annotations, so a preview
|
|
806
|
+
* never waits on a workspace write (rollback, undo and redo report one), and
|
|
807
|
+
* may show a withheld stub before its place is added; the activation waits
|
|
808
|
+
* for them around its call.
|
|
782
809
|
*/
|
|
783
810
|
async buildActivationRequest(
|
|
784
811
|
availableTools: ToolDefinition[],
|
|
785
812
|
injections?: ContextInjection[],
|
|
786
|
-
budget?: TokenBudget
|
|
813
|
+
budget?: TokenBudget,
|
|
814
|
+
compressionTools: ToolDefinition[] = availableTools,
|
|
815
|
+
options: ActivationRequestOptions = {},
|
|
787
816
|
): Promise<NormalizedRequest> {
|
|
788
|
-
//
|
|
817
|
+
// Compression may revisit hidden tools in recorded history. Keep its full
|
|
818
|
+
// definition set separate from the resident's advertised tools: the
|
|
789
819
|
// autobiographical strategy must declare the same tools on its
|
|
790
820
|
// summarizer/compression requests, or transcripts containing tool blocks
|
|
791
821
|
// are refused by Anthropic's reasoning_extraction classifier (labclaude
|
|
792
822
|
// incident, 2026-07-09). Optional chaining: older context-manager
|
|
793
823
|
// versions don't have the hook.
|
|
794
824
|
(this.contextManager as unknown as { setToolDefinitions?: (t: ToolDefinition[]) => void })
|
|
795
|
-
.setToolDefinitions?.(
|
|
825
|
+
.setToolDefinitions?.(compressionTools);
|
|
796
826
|
|
|
797
827
|
const strategy = (this.contextManager as unknown as { getStrategy?: () => unknown })
|
|
798
828
|
.getStrategy?.() as {
|
|
@@ -837,10 +867,24 @@ export class Agent {
|
|
|
837
867
|
}))
|
|
838
868
|
.filter((m) => m.content.length > 0);
|
|
839
869
|
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
|
|
870
|
+
// Silent heartbeat ticks store no prompt row. Render a request-only
|
|
871
|
+
// separator before each tick's first surviving row so the wire formatter
|
|
872
|
+
// cannot merge the tick into the previous assistant message (see
|
|
873
|
+
// silent-heartbeat.ts for what can and cannot be matched). This agent's
|
|
874
|
+
// own first request for a tick ends on that same turn, so the prefix it
|
|
875
|
+
// caches is the one every later compile renders. "First" is per agent: a
|
|
876
|
+
// broadcast tick shares its eventId with every resident.
|
|
877
|
+
const ticks = this.indexSilentHeartbeatTicks();
|
|
878
|
+
messages = separateSilentHeartbeatTicks(messages, ticks.rows, this.name);
|
|
879
|
+
const openingTick = options.silentHeartbeat !== undefined
|
|
880
|
+
&& !ticks.started.has(options.silentHeartbeat.eventId);
|
|
881
|
+
|
|
882
|
+
if (openingTick) {
|
|
883
|
+
messages = [...messages, silentHeartbeatSeparatorTurn()];
|
|
884
|
+
} else if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
|
|
885
|
+
// Safety: ensure messages don't end with an assistant message.
|
|
886
|
+
// Some models reject trailing assistant messages ("prefill not supported"),
|
|
887
|
+
// and after context compression a stale assistant turn can end up last.
|
|
844
888
|
messages = [...messages, {
|
|
845
889
|
participant: 'user',
|
|
846
890
|
content: [{ type: 'text', text: '[Continue]' }],
|
|
@@ -866,6 +910,26 @@ export class Agent {
|
|
|
866
910
|
};
|
|
867
911
|
}
|
|
868
912
|
|
|
913
|
+
/** Stored silent-tick rows, memoized on the store's cached message array
|
|
914
|
+
* (ContextManager returns the same array until the store changes). */
|
|
915
|
+
private silentTickIndex: { source: readonly unknown[]; index: SilentTickIndex } | null = null;
|
|
916
|
+
|
|
917
|
+
private indexSilentHeartbeatTicks(): SilentTickIndex {
|
|
918
|
+
const empty: SilentTickIndex = { rows: new Map(), started: new Set() };
|
|
919
|
+
const cm = this.contextManager as Partial<ContextManager>;
|
|
920
|
+
if (typeof cm.getAllMessages !== 'function') return empty;
|
|
921
|
+
try {
|
|
922
|
+
const stored = cm.getAllMessages() as unknown as readonly StoredRowLike[];
|
|
923
|
+
if (this.silentTickIndex?.source === stored) return this.silentTickIndex.index;
|
|
924
|
+
const index = indexSilentTickRows(stored, this.name);
|
|
925
|
+
this.silentTickIndex = { source: stored, index };
|
|
926
|
+
return index;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
console.error(`[silent-heartbeat] ${this.name}: could not index tick rows; separators omitted:`, error);
|
|
929
|
+
return empty;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
869
933
|
/**
|
|
870
934
|
* Start a yielding stream with context injections.
|
|
871
935
|
* Same as startStream but passes injections through to compile.
|
|
@@ -873,7 +937,9 @@ export class Agent {
|
|
|
873
937
|
async startStreamWithInjections(
|
|
874
938
|
availableTools: ToolDefinition[],
|
|
875
939
|
injections?: ContextInjection[],
|
|
876
|
-
budget?: TokenBudget
|
|
940
|
+
budget?: TokenBudget,
|
|
941
|
+
compressionTools: ToolDefinition[] = availableTools,
|
|
942
|
+
options: ActivationRequestOptions = {},
|
|
877
943
|
): Promise<StartStreamResult> {
|
|
878
944
|
if (this._state.status !== 'idle') {
|
|
879
945
|
throw new Error(`Agent ${this.name} cannot start stream in state ${this._state.status}`);
|
|
@@ -908,7 +974,11 @@ export class Agent {
|
|
|
908
974
|
this.lastStreamRealInputTokens = 0;
|
|
909
975
|
this.lastStreamOutputTokens = 0;
|
|
910
976
|
|
|
911
|
-
|
|
977
|
+
// Compiled only once each withheld stub's annotation has finished,
|
|
978
|
+
// including one that began during the compile, and after a stub edit that
|
|
979
|
+
// failed has been retried (see afterAnnotations).
|
|
980
|
+
const request = await this.toolResultGuard.afterAnnotations(
|
|
981
|
+
() => this.buildActivationRequest(availableTools, injections, budget, compressionTools, options));
|
|
912
982
|
request.messages = this.toolResultGuard.prepareRequest(request.messages, true);
|
|
913
983
|
|
|
914
984
|
const receiptAware = (this.contextManager as unknown as { getStrategy?: () => unknown })
|
|
@@ -1009,7 +1079,7 @@ export class Agent {
|
|
|
1009
1079
|
* Add an assistant response to context.
|
|
1010
1080
|
* Called by framework when stream completes.
|
|
1011
1081
|
*/
|
|
1012
|
-
addAssistantResponse(content: ContentBlock[]): void {
|
|
1082
|
+
addAssistantResponse(content: ContentBlock[], metadata?: MessageMetadata): void {
|
|
1013
1083
|
// A turn whose entire output is thinking blocks produced NOTHING: no
|
|
1014
1084
|
// speech, no tool call. That is what a refusal looks like on the wire —
|
|
1015
1085
|
// the provider returns signed thinking (often with empty text, the
|
|
@@ -1035,7 +1105,7 @@ export class Agent {
|
|
|
1035
1105
|
);
|
|
1036
1106
|
return;
|
|
1037
1107
|
}
|
|
1038
|
-
this.contextManager.addMessage(this.name, content);
|
|
1108
|
+
this.contextManager.addMessage(this.name, content, metadata);
|
|
1039
1109
|
}
|
|
1040
1110
|
|
|
1041
1111
|
/**
|
|
@@ -1126,9 +1196,13 @@ export class Agent {
|
|
|
1126
1196
|
|
|
1127
1197
|
private async doInference(
|
|
1128
1198
|
request: NormalizedRequest,
|
|
1129
|
-
signal
|
|
1199
|
+
signal: AbortSignal | undefined,
|
|
1200
|
+
/** Compiles the context into a request again, for the guard's retry. */
|
|
1201
|
+
reassemble: () => Promise<NormalizedRequest>,
|
|
1130
1202
|
): Promise<InferenceResult> {
|
|
1131
|
-
let response =
|
|
1203
|
+
let response = signal?.aborted
|
|
1204
|
+
? { aborted: true as const, reason: 'user' as const }
|
|
1205
|
+
: await this.membrane.stream(request, { signal });
|
|
1132
1206
|
// Usage of a refused attempt abandoned by the guard; it was billed and
|
|
1133
1207
|
// is added to the retry's usage below rather than overwritten.
|
|
1134
1208
|
let abandonedUsage: { inputTokens: number; outputTokens: number } | undefined;
|
|
@@ -1138,12 +1212,14 @@ export class Agent {
|
|
|
1138
1212
|
const ids = this.toolResultGuard.withhold('unknown');
|
|
1139
1213
|
if (ids) {
|
|
1140
1214
|
abandonedUsage = response.usage;
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
response =
|
|
1215
|
+
// The retry is compiled afresh, like every request: once each stub's
|
|
1216
|
+
// annotation has finished (agent-framework #277), and against the
|
|
1217
|
+
// budget without the batch's reservation, so a stub longer than its
|
|
1218
|
+
// original can't carry the request past the budget.
|
|
1219
|
+
const retry = await reassemble();
|
|
1220
|
+
response = signal?.aborted
|
|
1221
|
+
? { aborted: true, reason: 'user', partialUsage: response.usage }
|
|
1222
|
+
: await this.membrane.stream(retry, { signal });
|
|
1147
1223
|
}
|
|
1148
1224
|
}
|
|
1149
1225
|
|
|
@@ -1224,7 +1300,7 @@ export class Agent {
|
|
|
1224
1300
|
toolUseId: r.id,
|
|
1225
1301
|
content: r.result.isError
|
|
1226
1302
|
? truncateForHistory(r.result.error ?? 'Unknown error', DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS)
|
|
1227
|
-
:
|
|
1303
|
+
: nonErrorResultText(r.result, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS),
|
|
1228
1304
|
isError: r.result.isError,
|
|
1229
1305
|
}));
|
|
1230
1306
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* This is a ROBUSTNESS boundary, not a security sandbox — same doctrine as
|
|
11
11
|
* GateScript: the agent already has broader host access through its tools.
|
|
12
12
|
* What this class guarantees is liveness: a wedged or runaway script cannot
|
|
13
|
-
* hang the agent turn (cancel -> grace -> SIGKILL -> respawn) and a crashed
|
|
13
|
+
* hang the agent turn (cancel + SIGINT -> grace -> SIGKILL -> respawn) and a crashed
|
|
14
14
|
* interpreter surfaces as a tool result, never as an unhandled rejection.
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -65,6 +65,8 @@ export interface PyRunnerOptions {
|
|
|
65
65
|
toolCallTimeoutMs?: number;
|
|
66
66
|
/** Whole-script deadline; exceeded -> cancel, grace, kill. */
|
|
67
67
|
scriptTimeoutMs?: number;
|
|
68
|
+
/** How long a script gets to stop after its deadline before the interpreter is killed (default 10s). */
|
|
69
|
+
cancelGraceMs?: number;
|
|
68
70
|
/** Idle interpreter reclaim (state lost), mirroring container reclaim. */
|
|
69
71
|
idleReclaimMs?: number;
|
|
70
72
|
onToolCall: ScriptToolCallHandler;
|
|
@@ -76,6 +78,8 @@ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 270_000;
|
|
|
76
78
|
const DEFAULT_SCRIPT_TIMEOUT_MS = 600_000;
|
|
77
79
|
const DEFAULT_IDLE_RECLAIM_MS = 300_000;
|
|
78
80
|
const CANCEL_GRACE_MS = 10_000;
|
|
81
|
+
/** After the deadline, SIGINT is repeated at this interval until the script reports or is killed. */
|
|
82
|
+
const INTERRUPT_REPEAT_MS = 200;
|
|
79
83
|
|
|
80
84
|
/** The longest delay Node's timers honour (~24.8 days); a longer one fires after ~1 ms. */
|
|
81
85
|
export const MAX_TIMER_MS = 2_147_483_647;
|
|
@@ -92,6 +96,8 @@ interface PendingExec {
|
|
|
92
96
|
resolve: (result: ExecResult) => void;
|
|
93
97
|
deadlineTimer: ReturnType<typeof setTimeout> | null;
|
|
94
98
|
killTimer: ReturnType<typeof setTimeout> | null;
|
|
99
|
+
/** Repeats the deadline SIGINT (see interruptChild). */
|
|
100
|
+
interruptTimer?: ReturnType<typeof setInterval>;
|
|
95
101
|
settled: boolean;
|
|
96
102
|
/** Set once the deadline fired: the result then says the script ran out of time. */
|
|
97
103
|
deadlineMs: number | null;
|
|
@@ -101,6 +107,7 @@ export class PyRunner {
|
|
|
101
107
|
private readonly pythonPath: string;
|
|
102
108
|
private readonly toolCallTimeoutMs: number;
|
|
103
109
|
private readonly scriptTimeoutMs: number;
|
|
110
|
+
private readonly cancelGraceMs: number;
|
|
104
111
|
private readonly idleReclaimMs: number;
|
|
105
112
|
private readonly onToolCall: ScriptToolCallHandler;
|
|
106
113
|
private readonly label: string;
|
|
@@ -114,18 +121,21 @@ export class PyRunner {
|
|
|
114
121
|
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
115
122
|
private execCounter = 0;
|
|
116
123
|
private disposed = false;
|
|
124
|
+
/** An exec has passed the busy check but is still awaiting the interpreter. */
|
|
125
|
+
private starting = false;
|
|
117
126
|
|
|
118
127
|
constructor(options: PyRunnerOptions) {
|
|
119
128
|
this.pythonPath = options.pythonPath ?? 'python3';
|
|
120
129
|
this.toolCallTimeoutMs = options.toolCallTimeoutMs ?? DEFAULT_TOOL_CALL_TIMEOUT_MS;
|
|
121
130
|
this.scriptTimeoutMs = options.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS;
|
|
131
|
+
this.cancelGraceMs = options.cancelGraceMs ?? CANCEL_GRACE_MS;
|
|
122
132
|
this.idleReclaimMs = options.idleReclaimMs ?? DEFAULT_IDLE_RECLAIM_MS;
|
|
123
133
|
this.onToolCall = options.onToolCall;
|
|
124
134
|
this.label = options.label ?? 'pytc';
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
get busy(): boolean {
|
|
128
|
-
return this.pending !== null;
|
|
138
|
+
return this.pending !== null || this.starting;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
/**
|
|
@@ -150,7 +160,7 @@ export class PyRunner {
|
|
|
150
160
|
if (this.disposed) {
|
|
151
161
|
return { stdout: '', stderr: 'code_execution runner disposed', returnCode: 1, aborted: true };
|
|
152
162
|
}
|
|
153
|
-
if (this.
|
|
163
|
+
if (this.busy) {
|
|
154
164
|
return {
|
|
155
165
|
stdout: '',
|
|
156
166
|
stderr: 'RuntimeError: another code_execution script is already running for this agent',
|
|
@@ -160,6 +170,7 @@ export class PyRunner {
|
|
|
160
170
|
}
|
|
161
171
|
this.clearIdleTimer();
|
|
162
172
|
|
|
173
|
+
this.starting = true; // claimed before the await: a same-tick second exec must see it
|
|
163
174
|
try {
|
|
164
175
|
await this.ensureChild();
|
|
165
176
|
} catch (err) {
|
|
@@ -171,6 +182,8 @@ export class PyRunner {
|
|
|
171
182
|
returnCode: 1,
|
|
172
183
|
aborted: true,
|
|
173
184
|
};
|
|
185
|
+
} finally {
|
|
186
|
+
this.starting = false; // `pending` is set below, with no await in between
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
const execId = `e${++this.execCounter}`;
|
|
@@ -193,16 +206,26 @@ export class PyRunner {
|
|
|
193
206
|
// Deadline: ask politely first (script sees CancelledError and its
|
|
194
207
|
// exec_result still flows back), then kill on unresponsiveness.
|
|
195
208
|
this.send({ op: 'cancel', id: execId, reason: 'deadline' });
|
|
209
|
+
// The cancel lands only when the script awaits. Blocking code (time.sleep, a
|
|
210
|
+
// busy loop, a blocking read) never does, so interrupt it as well: the runtime
|
|
211
|
+
// raises KeyboardInterrupt in the script's own code, or schedules the
|
|
212
|
+
// cancellation of a script that is waiting. Repeated, because a waiting script
|
|
213
|
+
// can resume into blocking code before that cancellation runs; the runtime
|
|
214
|
+
// ignores the repeats once the script was interrupted or cancelled.
|
|
215
|
+
this.interruptChild();
|
|
216
|
+
pending.interruptTimer = setInterval(() => this.interruptChild(), INTERRUPT_REPEAT_MS);
|
|
196
217
|
pending.killTimer = setTimeout(() => {
|
|
197
218
|
pending.deadlineMs = null; // this message already says why
|
|
198
219
|
this.settlePending({
|
|
199
220
|
stdout: '',
|
|
200
|
-
stderr:
|
|
221
|
+
stderr:
|
|
222
|
+
`script stopped: it reached its ${formatLimit(deadlineMs)} time limit and did not respond, ` +
|
|
223
|
+
'so it was killed and the interpreter restarted (variables from earlier scripts are gone)',
|
|
201
224
|
returnCode: 1,
|
|
202
225
|
aborted: true,
|
|
203
226
|
});
|
|
204
227
|
this.reclaim('deadline-kill');
|
|
205
|
-
},
|
|
228
|
+
}, this.cancelGraceMs);
|
|
206
229
|
}, deadlineMs);
|
|
207
230
|
// A day-scale background deadline must not hold the process open.
|
|
208
231
|
if (background) pending.deadlineTimer.unref?.();
|
|
@@ -299,7 +322,7 @@ export class PyRunner {
|
|
|
299
322
|
});
|
|
300
323
|
|
|
301
324
|
this.reader = createInterface({ input: child.stdout });
|
|
302
|
-
this.reader.on('line', (line) => this.handleLine(line));
|
|
325
|
+
this.reader.on('line', (line) => this.handleLine(line, child));
|
|
303
326
|
|
|
304
327
|
this.childReady = new Promise<void>((resolve, reject) => {
|
|
305
328
|
const onReady = () => {
|
|
@@ -333,7 +356,7 @@ export class PyRunner {
|
|
|
333
356
|
|
|
334
357
|
private readyResolver: (() => void) | null = null;
|
|
335
358
|
|
|
336
|
-
private handleLine(line: string): void {
|
|
359
|
+
private handleLine(line: string, child: ChildProcessWithoutNullStreams): void {
|
|
337
360
|
let msg: { op?: string; id?: string; name?: string; args?: unknown; stdout?: string; stderr?: string; return_code?: number };
|
|
338
361
|
try {
|
|
339
362
|
msg = JSON.parse(line);
|
|
@@ -358,7 +381,7 @@ export class PyRunner {
|
|
|
358
381
|
this.onToolCall(toolName, args)
|
|
359
382
|
.catch((err) => `Error: ${err instanceof Error ? err.message : String(err)}`)
|
|
360
383
|
.then((result) => {
|
|
361
|
-
this.
|
|
384
|
+
this.reply(child, { op: 'tool_result', id: callId, result }, `result of ${toolName} call ${callId}`);
|
|
362
385
|
});
|
|
363
386
|
return;
|
|
364
387
|
}
|
|
@@ -376,7 +399,7 @@ export class PyRunner {
|
|
|
376
399
|
`wake handler failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
377
400
|
: Promise.resolve('this script is not allowed to wake the agent');
|
|
378
401
|
void refuse.then((error) => {
|
|
379
|
-
this.
|
|
402
|
+
this.reply(child, { op: 'wake_ack', id: wakeId, ...(error ? { error } : {}) }, `ack of wake ${wakeId}`);
|
|
380
403
|
});
|
|
381
404
|
return;
|
|
382
405
|
}
|
|
@@ -414,10 +437,25 @@ export class PyRunner {
|
|
|
414
437
|
}
|
|
415
438
|
if (pending.deadlineTimer) clearTimeout(pending.deadlineTimer);
|
|
416
439
|
if (pending.killTimer) clearTimeout(pending.killTimer);
|
|
440
|
+
if (pending.interruptTimer) clearInterval(pending.interruptTimer);
|
|
417
441
|
this.pending = null;
|
|
418
442
|
pending.resolve(result);
|
|
419
443
|
}
|
|
420
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Answer the interpreter that asked. Call and wake ids restart in every
|
|
447
|
+
* interpreter, so an answer that outlives its interpreter (the script was
|
|
448
|
+
* aborted, killed or crashed, or the interpreter reclaimed) would resolve
|
|
449
|
+
* the same id in the one that replaced it, inside another script: drop it.
|
|
450
|
+
*/
|
|
451
|
+
private reply(child: ChildProcessWithoutNullStreams, msg: unknown, what: string): void {
|
|
452
|
+
if (child !== this.child) {
|
|
453
|
+
console.error(`[pytc:${this.label}] dropped late ${what}: the interpreter that asked for it is gone`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.send(msg);
|
|
457
|
+
}
|
|
458
|
+
|
|
421
459
|
private send(obj: unknown): void {
|
|
422
460
|
const child = this.child;
|
|
423
461
|
if (!child || child.exitCode !== null || child.killed) return;
|
|
@@ -430,6 +468,25 @@ export class PyRunner {
|
|
|
430
468
|
}
|
|
431
469
|
}
|
|
432
470
|
|
|
471
|
+
/**
|
|
472
|
+
* SIGINT the interpreter: blocking code in the running script raises
|
|
473
|
+
* KeyboardInterrupt. Not on Windows, where Node's kill() ends the process
|
|
474
|
+
* outright; there the cancel op and the kill grace remain.
|
|
475
|
+
* process.kill rather than child.kill, which would mark the child `killed`:
|
|
476
|
+
* a killed child gets no more protocol messages and is respawned on the
|
|
477
|
+
* next exec, losing the interpreter state this keeps.
|
|
478
|
+
*/
|
|
479
|
+
private interruptChild(): void {
|
|
480
|
+
const child = this.child;
|
|
481
|
+
if (process.platform === 'win32' || !child || child.exitCode !== null || child.killed) return;
|
|
482
|
+
if (child.pid === undefined) return;
|
|
483
|
+
try {
|
|
484
|
+
process.kill(child.pid, 'SIGINT');
|
|
485
|
+
} catch {
|
|
486
|
+
// already gone: the exit handler settles the exec
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
433
490
|
/** Kill interpreter + clean temp dir. State is lost (by design). */
|
|
434
491
|
private reclaim(reason: string): void {
|
|
435
492
|
if (this.child) {
|