@jungjaehoon/mama-os 0.51.2 → 0.52.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/dist/agent/agent-loop.d.ts +50 -0
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +480 -138
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/code-act/constants.d.ts +1 -1
- package/dist/agent/code-act/constants.d.ts.map +1 -1
- package/dist/agent/code-act/constants.js +5 -2
- package/dist/agent/code-act/constants.js.map +1 -1
- package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
- package/dist/agent/code-act/host-bridge.js +102 -4
- package/dist/agent/code-act/host-bridge.js.map +1 -1
- package/dist/agent/code-act/tool-catalog.d.ts +2 -0
- package/dist/agent/code-act/tool-catalog.d.ts.map +1 -1
- package/dist/agent/code-act/tool-catalog.js +20 -7
- package/dist/agent/code-act/tool-catalog.js.map +1 -1
- package/dist/agent/codex-app-server-process.d.ts +112 -0
- package/dist/agent/codex-app-server-process.d.ts.map +1 -1
- package/dist/agent/codex-app-server-process.js +518 -1
- package/dist/agent/codex-app-server-process.js.map +1 -1
- package/dist/agent/gateway-tool-executor.d.ts +17 -0
- package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
- package/dist/agent/gateway-tool-executor.js +236 -53
- package/dist/agent/gateway-tool-executor.js.map +1 -1
- package/dist/agent/gateway-tools.md +9 -3
- package/dist/agent/model-runner.d.ts +5 -0
- package/dist/agent/model-runner.d.ts.map +1 -1
- package/dist/agent/model-runner.js.map +1 -1
- package/dist/agent/skill-loader.d.ts +7 -2
- package/dist/agent/skill-loader.d.ts.map +1 -1
- package/dist/agent/skill-loader.js +29 -8
- package/dist/agent/skill-loader.js.map +1 -1
- package/dist/agent/subagent-run-reconcile.d.ts +23 -0
- package/dist/agent/subagent-run-reconcile.d.ts.map +1 -0
- package/dist/agent/subagent-run-reconcile.js +56 -0
- package/dist/agent/subagent-run-reconcile.js.map +1 -0
- package/dist/agent/tool-registry.d.ts.map +1 -1
- package/dist/agent/tool-registry.js +126 -3
- package/dist/agent/tool-registry.js.map +1 -1
- package/dist/agent/types.d.ts +52 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/graph-api.d.ts.map +1 -1
- package/dist/api/graph-api.js +0 -22
- package/dist/api/graph-api.js.map +1 -1
- package/dist/cli/commands/start.d.ts +8 -7
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +148 -74
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js +2 -1
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/runtime/api-routes-init.d.ts +1 -0
- package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
- package/dist/cli/runtime/api-routes-init.js +76 -32
- package/dist/cli/runtime/api-routes-init.js.map +1 -1
- package/dist/db/migrations/operator-procedures.d.ts +4 -0
- package/dist/db/migrations/operator-procedures.d.ts.map +1 -0
- package/dist/db/migrations/operator-procedures.js +63 -0
- package/dist/db/migrations/operator-procedures.js.map +1 -0
- package/dist/gateways/message-router.d.ts +4 -3
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +43 -131
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/gateways/telegram-format.d.ts.map +1 -1
- package/dist/gateways/telegram-format.js +2 -0
- package/dist/gateways/telegram-format.js.map +1 -1
- package/dist/multi-agent/runtime-process.d.ts +8 -0
- package/dist/multi-agent/runtime-process.d.ts.map +1 -1
- package/dist/multi-agent/runtime-process.js +6 -0
- package/dist/multi-agent/runtime-process.js.map +1 -1
- package/dist/operator/board-delta-gate.d.ts +34 -10
- package/dist/operator/board-delta-gate.d.ts.map +1 -1
- package/dist/operator/board-delta-gate.js +68 -24
- package/dist/operator/board-delta-gate.js.map +1 -1
- package/dist/operator/board-refresh-gate.d.ts +11 -6
- package/dist/operator/board-refresh-gate.d.ts.map +1 -1
- package/dist/operator/board-refresh-gate.js +12 -11
- package/dist/operator/board-refresh-gate.js.map +1 -1
- package/dist/operator/board-slot-instructions.d.ts +30 -0
- package/dist/operator/board-slot-instructions.d.ts.map +1 -1
- package/dist/operator/board-slot-instructions.js +110 -6
- package/dist/operator/board-slot-instructions.js.map +1 -1
- package/dist/operator/console-brief.d.ts +26 -13
- package/dist/operator/console-brief.d.ts.map +1 -1
- package/dist/operator/console-brief.js +159 -83
- package/dist/operator/console-brief.js.map +1 -1
- package/dist/operator/experience-evidence.d.ts +18 -0
- package/dist/operator/experience-evidence.d.ts.map +1 -0
- package/dist/operator/experience-evidence.js +82 -0
- package/dist/operator/experience-evidence.js.map +1 -0
- package/dist/operator/experience-hints.d.ts +44 -0
- package/dist/operator/experience-hints.d.ts.map +1 -0
- package/dist/operator/experience-hints.js +98 -0
- package/dist/operator/experience-hints.js.map +1 -0
- package/dist/operator/mama-memory-port.js +1 -3
- package/dist/operator/mama-memory-port.js.map +1 -1
- package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
- package/dist/operator/operator-trigger-loop.js +3 -2
- package/dist/operator/operator-trigger-loop.js.map +1 -1
- package/dist/operator/owner-action-effects.d.ts +13 -1
- package/dist/operator/owner-action-effects.d.ts.map +1 -1
- package/dist/operator/owner-action-effects.js +17 -4
- package/dist/operator/owner-action-effects.js.map +1 -1
- package/dist/operator/owner-event-inbox.d.ts +7 -1
- package/dist/operator/owner-event-inbox.d.ts.map +1 -1
- package/dist/operator/owner-event-inbox.js +8 -0
- package/dist/operator/owner-event-inbox.js.map +1 -1
- package/dist/operator/owner-event-loop.d.ts +28 -2
- package/dist/operator/owner-event-loop.d.ts.map +1 -1
- package/dist/operator/owner-event-loop.js +60 -2
- package/dist/operator/owner-event-loop.js.map +1 -1
- package/dist/operator/owner-event-policy.d.ts +20 -0
- package/dist/operator/owner-event-policy.d.ts.map +1 -1
- package/dist/operator/owner-event-policy.js +34 -5
- package/dist/operator/owner-event-policy.js.map +1 -1
- package/dist/operator/owner-event-prompt.d.ts +13 -4
- package/dist/operator/owner-event-prompt.d.ts.map +1 -1
- package/dist/operator/owner-event-prompt.js +31 -38
- package/dist/operator/owner-event-prompt.js.map +1 -1
- package/dist/operator/owner-runtime-journal.d.ts.map +1 -1
- package/dist/operator/owner-runtime-journal.js +7 -3
- package/dist/operator/owner-runtime-journal.js.map +1 -1
- package/dist/operator/owner-runtime.d.ts +6 -0
- package/dist/operator/owner-runtime.d.ts.map +1 -1
- package/dist/operator/owner-runtime.js +48 -5
- package/dist/operator/owner-runtime.js.map +1 -1
- package/dist/operator/procedure-activation.d.ts +7 -0
- package/dist/operator/procedure-activation.d.ts.map +1 -0
- package/dist/operator/procedure-activation.js +132 -0
- package/dist/operator/procedure-activation.js.map +1 -0
- package/dist/operator/procedure-projection.d.ts +25 -0
- package/dist/operator/procedure-projection.d.ts.map +1 -0
- package/dist/operator/procedure-projection.js +80 -0
- package/dist/operator/procedure-projection.js.map +1 -0
- package/dist/operator/procedure-runtime.d.ts +45 -0
- package/dist/operator/procedure-runtime.d.ts.map +1 -0
- package/dist/operator/procedure-runtime.js +496 -0
- package/dist/operator/procedure-runtime.js.map +1 -0
- package/dist/operator/procedure-store.d.ts +97 -0
- package/dist/operator/procedure-store.d.ts.map +1 -0
- package/dist/operator/procedure-store.js +368 -0
- package/dist/operator/procedure-store.js.map +1 -0
- package/dist/operator/subagent-stimulus.d.ts +34 -0
- package/dist/operator/subagent-stimulus.d.ts.map +1 -0
- package/dist/operator/subagent-stimulus.js +145 -0
- package/dist/operator/subagent-stimulus.js.map +1 -0
- package/dist/operator/task-ledger.d.ts +40 -3
- package/dist/operator/task-ledger.d.ts.map +1 -1
- package/dist/operator/task-ledger.js +63 -10
- package/dist/operator/task-ledger.js.map +1 -1
- package/dist/operator/thread-brief-memory.d.ts +29 -0
- package/dist/operator/thread-brief-memory.d.ts.map +1 -0
- package/dist/operator/thread-brief-memory.js +61 -0
- package/dist/operator/thread-brief-memory.js.map +1 -0
- package/dist/operator/trigger-author.d.ts +2 -1
- package/dist/operator/trigger-author.d.ts.map +1 -1
- package/dist/operator/trigger-author.js +23 -0
- package/dist/operator/trigger-author.js.map +1 -1
- package/dist/operator/trigger-matcher.js +1 -0
- package/dist/operator/trigger-matcher.js.map +1 -1
- package/dist/operator/trigger-registry.d.ts +5 -4
- package/dist/operator/trigger-registry.d.ts.map +1 -1
- package/dist/operator/trigger-registry.js +78 -24
- package/dist/operator/trigger-registry.js.map +1 -1
- package/dist/operator/trigger-review.d.ts +1 -1
- package/dist/operator/trigger-review.d.ts.map +1 -1
- package/dist/operator/trigger-review.js +24 -4
- package/dist/operator/trigger-review.js.map +1 -1
- package/dist/operator/trigger-types.d.ts +10 -1
- package/dist/operator/trigger-types.d.ts.map +1 -1
- package/dist/operator/worker-run.d.ts +7 -1
- package/dist/operator/worker-run.d.ts.map +1 -1
- package/dist/operator/worker-run.js +5 -2
- package/dist/operator/worker-run.js.map +1 -1
- package/dist/operator/workorder-consumer.d.ts +91 -14
- package/dist/operator/workorder-consumer.d.ts.map +1 -1
- package/dist/operator/workorder-consumer.js +272 -93
- package/dist/operator/workorder-consumer.js.map +1 -1
- package/dist/operator/workorder-hooks.d.ts +42 -9
- package/dist/operator/workorder-hooks.d.ts.map +1 -1
- package/dist/operator/workorder-hooks.js +52 -13
- package/dist/operator/workorder-hooks.js.map +1 -1
- package/dist/operator/workorder-publishers.d.ts +9 -1
- package/dist/operator/workorder-publishers.d.ts.map +1 -1
- package/dist/operator/workorder-publishers.js +21 -2
- package/dist/operator/workorder-publishers.js.map +1 -1
- package/package.json +2 -2
- package/dist/operator/learning-context.d.ts +0 -49
- package/dist/operator/learning-context.d.ts.map +0 -1
- package/dist/operator/learning-context.js +0 -129
- package/dist/operator/learning-context.js.map +0 -1
- package/dist/operator/learning-markers.d.ts +0 -19
- package/dist/operator/learning-markers.d.ts.map +0 -1
- package/dist/operator/learning-markers.js +0 -147
- package/dist/operator/learning-markers.js.map +0 -1
- package/dist/operator/learning-read.d.ts +0 -16
- package/dist/operator/learning-read.d.ts.map +0 -1
- package/dist/operator/learning-read.js +0 -18
- package/dist/operator/learning-read.js.map +0 -1
- package/dist/operator/turn-observer.d.ts +0 -34
- package/dist/operator/turn-observer.d.ts.map +0 -1
- package/dist/operator/turn-observer.js +0 -79
- package/dist/operator/turn-observer.js.map +0 -1
- package/templates/skills/heartbeat-report.md +0 -75
package/dist/agent/agent-loop.js
CHANGED
|
@@ -69,6 +69,7 @@ const index_js_2 = require("../concurrency/index.js");
|
|
|
69
69
|
const session_pool_js_1 = require("./session-pool.js");
|
|
70
70
|
const principal_js_1 = require("../gateways/principal.js");
|
|
71
71
|
const owner_runtime_js_1 = require("../operator/owner-runtime.js");
|
|
72
|
+
const owner_event_policy_js_1 = require("../operator/owner-event-policy.js");
|
|
72
73
|
const os_1 = require("os");
|
|
73
74
|
const path_1 = require("path");
|
|
74
75
|
const crypto_1 = require("crypto");
|
|
@@ -278,31 +279,15 @@ function loadBackendAgentsMd(backend, verbose = false) {
|
|
|
278
279
|
return '';
|
|
279
280
|
}
|
|
280
281
|
function loadComposedSystemPrompt(verbose = false, context) {
|
|
281
|
-
const mamaHome = (0, path_1.join)((0, os_1.homedir)(), '.mama');
|
|
282
282
|
const layers = [];
|
|
283
|
-
// Load persona files: SOUL.md, IDENTITY.md, USER.md
|
|
284
|
-
const personaFiles = ['SOUL.md', 'IDENTITY.md', 'USER.md'];
|
|
285
|
-
for (const file of personaFiles) {
|
|
286
|
-
const path = (0, path_1.join)(mamaHome, file);
|
|
287
|
-
if ((0, fs_1.existsSync)(path)) {
|
|
288
|
-
if (verbose)
|
|
289
|
-
console.log(`[AgentLoop] Loaded persona: ${file}`);
|
|
290
|
-
const content = (0, fs_1.readFileSync)(path, 'utf-8');
|
|
291
|
-
layers.push(content);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
if (verbose)
|
|
295
|
-
console.log(`[AgentLoop] Persona file not found (skipping): ${file}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
283
|
// Load skill catalog (on-demand mode — full content injected per-message by PromptEnhancer)
|
|
299
|
-
const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(verbose), context);
|
|
284
|
+
const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(verbose, { includePaths: context?.roleName === 'owner_console' }), context);
|
|
300
285
|
if (skillCatalog.length > 0) {
|
|
301
286
|
const skillDirective = [
|
|
302
287
|
'# Installed Skills',
|
|
303
288
|
'',
|
|
304
|
-
'
|
|
305
|
-
'
|
|
289
|
+
'Choose relevant skills from their descriptions and the current task; keywords are optional hints.',
|
|
290
|
+
'Read the selected instructions (source path when listed) before applying them. Learned procedures use procedure_list/read.',
|
|
306
291
|
'',
|
|
307
292
|
...skillCatalog,
|
|
308
293
|
].join('\n');
|
|
@@ -344,6 +329,8 @@ function getRunGatewayToolsPrompt(context, disallowed) {
|
|
|
344
329
|
privateConnectorPolicy: DEFAULT_PRIVATE_CONNECTOR_POLICY,
|
|
345
330
|
}).prompt;
|
|
346
331
|
}
|
|
332
|
+
/** Korean/English phrasings that assert a correction, save, update or send has happened. */
|
|
333
|
+
const COMPLETION_CLAIM_PATTERN = /(?:반영|저장|기록|등록|갱신|적용|수정|전송|발송)\s*(?:완료|했습니다|됐습니다|되었습니다|하였습니다)|\b(?:saved|updated|applied|recorded|sent)\b/i;
|
|
347
334
|
const CANONICAL_CODE_ACT_HEADING = '## Code-Act: Gateway Tool Execution via Sandbox';
|
|
348
335
|
const CODE_ACT_MCP_COMPAT_NAME = 'mcp__code-act__code_act';
|
|
349
336
|
const GENERATED_CODE_ACT_START = '<!-- MAMA_GENERATED_CODE_ACT_START -->';
|
|
@@ -513,6 +500,34 @@ function withExecutionSurface(executionContext, executionSurface) {
|
|
|
513
500
|
executionSurface,
|
|
514
501
|
};
|
|
515
502
|
}
|
|
503
|
+
/** Normal coding tasks often need 10+ consecutive Bash calls; 5 was too low. */
|
|
504
|
+
const MAX_CONSECUTIVE_SAME_HOST_TOOL = 15;
|
|
505
|
+
/**
|
|
506
|
+
* The parent run's shape, kept only so a child announced mid-run (or after it) can be
|
|
507
|
+
* given an authority of its OWN: its own model run under the parent's, its own envelope,
|
|
508
|
+
* its own bridge with its own counters.
|
|
509
|
+
*/
|
|
510
|
+
/**
|
|
511
|
+
* The options a native child of this run inherits. Everything is carried EXCEPT the role,
|
|
512
|
+
* which is projected onto the unattended surface: the owner's 1:1 chat turn holds the
|
|
513
|
+
* gateway shell and file writer, and a child answers to no one inside the turn.
|
|
514
|
+
*/
|
|
515
|
+
function withUnattendedSubagentRole(options) {
|
|
516
|
+
const agentContext = options?.agentContext;
|
|
517
|
+
if (!agentContext) {
|
|
518
|
+
return { ...options };
|
|
519
|
+
}
|
|
520
|
+
const role = (0, owner_event_policy_js_1.projectUnattendedRole)(agentContext.role);
|
|
521
|
+
return {
|
|
522
|
+
...options,
|
|
523
|
+
agentContext: {
|
|
524
|
+
...agentContext,
|
|
525
|
+
role,
|
|
526
|
+
capabilities: [...role.allowedTools],
|
|
527
|
+
limitations: (role.blockedTools ?? []).map((tool) => `Cannot use ${tool}`),
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
516
531
|
class AgentLoop {
|
|
517
532
|
/**
|
|
518
533
|
* Construction-wide child-runtime tool capability.
|
|
@@ -526,6 +541,12 @@ class AgentLoop {
|
|
|
526
541
|
probesDurableSession;
|
|
527
542
|
ownerRecoveryJournalEnabled;
|
|
528
543
|
agent;
|
|
544
|
+
/**
|
|
545
|
+
* What a native child started by a run on this session needs to be given its own
|
|
546
|
+
* authority. Keyed by session key, overwritten by each run on that session, and dropped
|
|
547
|
+
* once the run finished and no child of it is still registered.
|
|
548
|
+
*/
|
|
549
|
+
subagentRunContexts = new Map();
|
|
529
550
|
persistentCLI = null;
|
|
530
551
|
mcpExecutor;
|
|
531
552
|
maxTurns;
|
|
@@ -622,27 +643,12 @@ class AgentLoop {
|
|
|
622
643
|
// Priority 1 (never cut): CLAUDE.md base instructions
|
|
623
644
|
// Priority 2 (cut if extreme): personas (SOUL, IDENTITY, USER) + gateway tools
|
|
624
645
|
// Priority 3 (cut first): context prompt + skills + onboarding
|
|
625
|
-
const mamaHome = (0, path_1.join)((0, os_1.homedir)(), '.mama');
|
|
626
646
|
const claudeMd = loadSystemPrompt();
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const p = (0, path_1.join)(mamaHome, file);
|
|
631
|
-
if ((0, fs_1.existsSync)(p))
|
|
632
|
-
personaParts.push((0, fs_1.readFileSync)(p, 'utf-8'));
|
|
633
|
-
}
|
|
634
|
-
const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(), options.agentContext ?? null);
|
|
647
|
+
const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(false, {
|
|
648
|
+
includePaths: options.agentContext?.roleName === 'owner_console',
|
|
649
|
+
}), options.agentContext ?? null);
|
|
635
650
|
promptLayers = [
|
|
636
651
|
{ name: 'claudeMd', content: claudeMd, priority: 1 },
|
|
637
|
-
...(personaParts.length > 0
|
|
638
|
-
? [
|
|
639
|
-
{
|
|
640
|
-
name: 'personas',
|
|
641
|
-
content: personaParts.join('\n\n---\n\n'),
|
|
642
|
-
priority: 2,
|
|
643
|
-
},
|
|
644
|
-
]
|
|
645
|
-
: []),
|
|
646
652
|
...(skillCatalog.length > 0
|
|
647
653
|
? [
|
|
648
654
|
{
|
|
@@ -650,7 +656,7 @@ class AgentLoop {
|
|
|
650
656
|
content: [
|
|
651
657
|
'# Installed Skills',
|
|
652
658
|
'',
|
|
653
|
-
'
|
|
659
|
+
'Choose skills by their purpose and the current task, not only literal keywords.',
|
|
654
660
|
'',
|
|
655
661
|
...skillCatalog,
|
|
656
662
|
].join('\n'),
|
|
@@ -750,6 +756,9 @@ class AgentLoop {
|
|
|
750
756
|
mcpConfigPath: this.useCodeAct
|
|
751
757
|
? undefined
|
|
752
758
|
: (options.mcpConfigPath ?? (useMCPMode ? mcpConfigPath : undefined)),
|
|
759
|
+
// A Codex-native child outlives the parent turn. The host - not the child's
|
|
760
|
+
// parent bridge - issues its authority: own model run, own envelope, own counters.
|
|
761
|
+
createSubagentBridge: (info) => this.createSubagentBridge(info),
|
|
753
762
|
});
|
|
754
763
|
logger.debug('Codex app-server backend enabled');
|
|
755
764
|
}
|
|
@@ -952,6 +961,15 @@ class AgentLoop {
|
|
|
952
961
|
setAgentEventBus(eventBus) {
|
|
953
962
|
this.mcpExecutor.setAgentEventBus(eventBus);
|
|
954
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* The model runner backing this loop, for host adapters that subscribe to runner
|
|
966
|
+
* lifecycle events (e.g. native subagent completion waking the standing owner).
|
|
967
|
+
* Returned as `unknown` on purpose: the runner surface differs per backend, so the
|
|
968
|
+
* caller must feature-detect rather than assume an emitter.
|
|
969
|
+
*/
|
|
970
|
+
getModelRunner() {
|
|
971
|
+
return this.agent;
|
|
972
|
+
}
|
|
955
973
|
/**
|
|
956
974
|
* Run the agent loop with a user prompt
|
|
957
975
|
*
|
|
@@ -1010,15 +1028,29 @@ class AgentLoop {
|
|
|
1010
1028
|
// Queue wait consumes no execution authority. Never renew a signed envelope implicitly:
|
|
1011
1029
|
// only a host-supplied issuer can prepare the current grant after both lane waits.
|
|
1012
1030
|
if (options?.prepareEnvelope) {
|
|
1031
|
+
const runEnvelopeIssuer = options.prepareEnvelope;
|
|
1013
1032
|
options = {
|
|
1014
1033
|
...options,
|
|
1015
|
-
envelope: await
|
|
1034
|
+
envelope: await runEnvelopeIssuer(),
|
|
1016
1035
|
prepareEnvelope: undefined,
|
|
1036
|
+
// A native child outlives this envelope and needs one of its OWN. The run's issuer
|
|
1037
|
+
// is the fallback when the host supplied no dedicated subagent issuer, so it is kept
|
|
1038
|
+
// reachable here instead of being lost with this rewrite - without it a child would
|
|
1039
|
+
// silently get no authority at all.
|
|
1040
|
+
prepareSubagentEnvelope: options.prepareSubagentEnvelope ?? (async () => await runEnvelopeIssuer()),
|
|
1017
1041
|
};
|
|
1018
1042
|
if (this.stopped) {
|
|
1019
1043
|
throw new types_js_1.AgentError('Agent loop is stopping', 'AGENT_STOPPED', undefined, false);
|
|
1020
1044
|
}
|
|
1021
1045
|
}
|
|
1046
|
+
if (options?.prepareContent) {
|
|
1047
|
+
const admitted = await options.prepareContent();
|
|
1048
|
+
content = admitted.content;
|
|
1049
|
+
options = { ...options, procedureRefs: admitted.procedureRefs, prepareContent: undefined };
|
|
1050
|
+
if (this.stopped) {
|
|
1051
|
+
throw new types_js_1.AgentError('Agent loop is stopping', 'AGENT_STOPPED', undefined, false);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1022
1054
|
if (options?.agentContext) {
|
|
1023
1055
|
options = {
|
|
1024
1056
|
...options,
|
|
@@ -1057,6 +1089,13 @@ class AgentLoop {
|
|
|
1057
1089
|
},
|
|
1058
1090
|
};
|
|
1059
1091
|
let toolExecutionContext = this.withBackgroundTaskRegistry(this.buildToolExecutionContext(options), backgroundTasks);
|
|
1092
|
+
if (toolExecutionContext) {
|
|
1093
|
+
toolExecutionContext = {
|
|
1094
|
+
...toolExecutionContext,
|
|
1095
|
+
procedureStimulus: ownerJournalPrompt,
|
|
1096
|
+
procedureRefs: options?.procedureRefs,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1060
1099
|
// Track this run's tier for code-act execution and prompt sizing.
|
|
1061
1100
|
if (options?.agentContext) {
|
|
1062
1101
|
const rawTier = options.agentContext.tier ?? 1;
|
|
@@ -1069,11 +1108,14 @@ class AgentLoop {
|
|
|
1069
1108
|
// Infinite loop prevention
|
|
1070
1109
|
let consecutiveToolCalls = 0;
|
|
1071
1110
|
let lastToolName = '';
|
|
1072
|
-
const MAX_CONSECUTIVE_SAME_TOOL =
|
|
1073
|
-
const EMERGENCY_MAX_TURNS =
|
|
1111
|
+
const MAX_CONSECUTIVE_SAME_TOOL = MAX_CONSECUTIVE_SAME_HOST_TOOL;
|
|
1112
|
+
const EMERGENCY_MAX_TURNS = this.emergencyMaxCalls(); // Always above maxTurns
|
|
1074
1113
|
// Track channel key for session release
|
|
1075
1114
|
const channelKey = options?.sessionKey ??
|
|
1076
1115
|
(0, session_pool_js_1.buildChannelKey)(options?.source ?? 'default', (0, principal_js_1.laneChannelId)(options?.channelId ?? this.sessionKey, 'owner'));
|
|
1116
|
+
// The context object THIS run created, compared by identity before the finally marks
|
|
1117
|
+
// it finished: a later run on the same session key replaces the map entry.
|
|
1118
|
+
let ownedSubagentRunContext;
|
|
1077
1119
|
// Use session pool for conversation continuity
|
|
1078
1120
|
// IMPORTANT: If caller passes cliSessionId, use it directly to avoid double-locking
|
|
1079
1121
|
// MessageRouter already calls getSession() and passes the result via options
|
|
@@ -1175,104 +1217,76 @@ class AgentLoop {
|
|
|
1175
1217
|
...options,
|
|
1176
1218
|
modelRunId: ownedModelRunId,
|
|
1177
1219
|
}), backgroundTasks);
|
|
1220
|
+
if (toolExecutionContext) {
|
|
1221
|
+
toolExecutionContext = {
|
|
1222
|
+
...toolExecutionContext,
|
|
1223
|
+
procedureStimulus: ownerJournalPrompt,
|
|
1224
|
+
procedureRefs: options?.procedureRefs,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1178
1227
|
}
|
|
1179
1228
|
nativeEffects = new native_effect_observer_js_1.NativeEffectReplayBoundary(this.createNativeEffectObserver?.(toolExecutionContext));
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
let
|
|
1229
|
+
// This run's bridge is ITS OWN: the loop guards, the emergency budget, the history
|
|
1230
|
+
// writes and the turn observers all belong to this run. A Codex-native child gets a
|
|
1231
|
+
// separate bridge from `createSubagentBridge` - sharing this closure let a child trip
|
|
1232
|
+
// the parent's loop guard and append its traces to the parent's committed model run.
|
|
1233
|
+
const hostToolDefinitions = this.hostToolDefinitionsFor(options, outerCodeActAllowed);
|
|
1183
1234
|
const hostToolBridge = isDurableRuntime && this.isGatewayMode
|
|
1184
|
-
? {
|
|
1185
|
-
tools:
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
nativeToolCallCount += 1;
|
|
1217
|
-
nativeConsecutiveToolCalls = nextConsecutiveCount;
|
|
1218
|
-
nativeLastToolSignature = toolSignature;
|
|
1219
|
-
const toolUse = {
|
|
1220
|
-
type: 'tool_use',
|
|
1221
|
-
id: call.callId,
|
|
1222
|
-
name: call.name,
|
|
1223
|
-
input: call.input,
|
|
1224
|
-
};
|
|
1225
|
-
// Codex does not return completed host exchanges, so record them
|
|
1226
|
-
// here. Cline reports the paired custom-tool exchange from its Hub
|
|
1227
|
-
// event stream and AgentLoop appends it exactly once below.
|
|
1228
|
-
if (isCodex) {
|
|
1229
|
-
history.push({ role: 'assistant', content: [toolUse] });
|
|
1230
|
-
runScope.onTurn?.({
|
|
1231
|
-
turn,
|
|
1232
|
-
role: 'assistant',
|
|
1233
|
-
content: [toolUse],
|
|
1234
|
-
stopReason: 'tool_use',
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
const callExecutionContext = toolExecutionContext
|
|
1238
|
-
? {
|
|
1239
|
-
...toolExecutionContext,
|
|
1240
|
-
gatewayCallId: call.callId,
|
|
1241
|
-
signal: callSignal,
|
|
1242
|
-
}
|
|
1243
|
-
: null;
|
|
1244
|
-
const [toolResult] = await this.executeTools([toolUse], options?.stopAfterSuccessfulTools ?? [], callExecutionContext, runScope);
|
|
1245
|
-
if (!toolResult) {
|
|
1246
|
-
callSignal.throwIfAborted();
|
|
1247
|
-
return {
|
|
1248
|
-
content: `Native tool "${call.name}" returned no result`,
|
|
1249
|
-
isError: true,
|
|
1250
|
-
abort: true,
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
if (!toolResult.terminalCode) {
|
|
1254
|
-
callSignal.throwIfAborted();
|
|
1255
|
-
}
|
|
1256
|
-
if (isCodex) {
|
|
1257
|
-
const persistedToolResult = historyToolResult(toolResult);
|
|
1258
|
-
history.push({ role: 'user', content: [persistedToolResult] });
|
|
1259
|
-
runScope.onTurn?.({
|
|
1260
|
-
turn,
|
|
1261
|
-
role: 'user',
|
|
1262
|
-
content: [persistedToolResult],
|
|
1263
|
-
});
|
|
1235
|
+
? this.buildHostToolBridge({
|
|
1236
|
+
tools: hostToolDefinitions,
|
|
1237
|
+
runScope,
|
|
1238
|
+
executionContext: () => toolExecutionContext,
|
|
1239
|
+
stopAfterSuccessfulTools: options?.stopAfterSuccessfulTools ?? [],
|
|
1240
|
+
emergencyMaxCalls: EMERGENCY_MAX_TURNS,
|
|
1241
|
+
maxConsecutiveSameTool: MAX_CONSECUTIVE_SAME_TOOL,
|
|
1242
|
+
// Codex does not return completed host exchanges, so record them here. Cline
|
|
1243
|
+
// reports the paired custom-tool exchange from its Hub event stream and
|
|
1244
|
+
// AgentLoop appends it exactly once below.
|
|
1245
|
+
...(isCodex
|
|
1246
|
+
? {
|
|
1247
|
+
recordExchange: {
|
|
1248
|
+
assistant: (toolUse) => {
|
|
1249
|
+
history.push({ role: 'assistant', content: [toolUse] });
|
|
1250
|
+
runScope.onTurn?.({
|
|
1251
|
+
turn,
|
|
1252
|
+
role: 'assistant',
|
|
1253
|
+
content: [toolUse],
|
|
1254
|
+
stopReason: 'tool_use',
|
|
1255
|
+
});
|
|
1256
|
+
},
|
|
1257
|
+
result: (toolResult) => {
|
|
1258
|
+
const persistedToolResult = historyToolResult(toolResult);
|
|
1259
|
+
history.push({ role: 'user', content: [persistedToolResult] });
|
|
1260
|
+
runScope.onTurn?.({
|
|
1261
|
+
turn,
|
|
1262
|
+
role: 'user',
|
|
1263
|
+
content: [persistedToolResult],
|
|
1264
|
+
});
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1264
1267
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
isError: toolResult.is_error === true,
|
|
1268
|
-
abort: toolResult.abort === true,
|
|
1269
|
-
terminalCode: toolResult.terminalCode,
|
|
1270
|
-
stop: toolResult.is_error !== true &&
|
|
1271
|
-
(options?.stopAfterSuccessfulTools ?? []).includes(call.name),
|
|
1272
|
-
};
|
|
1273
|
-
},
|
|
1274
|
-
}
|
|
1268
|
+
: {}),
|
|
1269
|
+
})
|
|
1275
1270
|
: undefined;
|
|
1271
|
+
if (hostToolBridge && isCodex) {
|
|
1272
|
+
// A child can be announced during this run and outlive it, so what its authority
|
|
1273
|
+
// needs is recorded now, per session key, and dropped when nothing needs it.
|
|
1274
|
+
// A previous run's context object stays referenced by ITS still-live children, so
|
|
1275
|
+
// this run starts its own count rather than inheriting one it can never settle.
|
|
1276
|
+
// The child NEVER inherits this turn's role verbatim: an owner 1:1 chat turn holds
|
|
1277
|
+
// the gateway shell and file writer, and a child is unattended by definition.
|
|
1278
|
+
const childOptions = withUnattendedSubagentRole(options);
|
|
1279
|
+
ownedSubagentRunContext = {
|
|
1280
|
+
options: childOptions,
|
|
1281
|
+
parentModelRunId: ownedModelRunId ?? options?.modelRunId ?? null,
|
|
1282
|
+
cliSessionId: resolvedCliSessionId,
|
|
1283
|
+
tools: this.hostToolDefinitionsFor(childOptions),
|
|
1284
|
+
tier: runScope.tier,
|
|
1285
|
+
activeChildren: 0,
|
|
1286
|
+
runFinished: false,
|
|
1287
|
+
};
|
|
1288
|
+
this.subagentRunContexts.set(channelKey, ownedSubagentRunContext);
|
|
1289
|
+
}
|
|
1276
1290
|
const prepareSystemPrompt = (requestedSystemPrompt, isResumingSession, includeOwnerRecovery = false) => {
|
|
1277
1291
|
let baseSystemPrompt = requestedSystemPrompt ?? this.defaultSystemPrompt;
|
|
1278
1292
|
if (ownerRuntime && !baseSystemPrompt.includes(owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS)) {
|
|
@@ -1281,6 +1295,9 @@ class AgentLoop {
|
|
|
1281
1295
|
if (ownerRuntime && !baseSystemPrompt.includes(telegram_format_js_1.TELEGRAM_FORMAT_GUIDE)) {
|
|
1282
1296
|
baseSystemPrompt = `${baseSystemPrompt}\n\n${telegram_format_js_1.TELEGRAM_FORMAT_GUIDE}`;
|
|
1283
1297
|
}
|
|
1298
|
+
if (ownerRuntime && !baseSystemPrompt.includes(owner_runtime_js_1.OWNER_RUNTIME_RULES)) {
|
|
1299
|
+
baseSystemPrompt = `${baseSystemPrompt}\n\n${owner_runtime_js_1.OWNER_RUNTIME_RULES}`;
|
|
1300
|
+
}
|
|
1284
1301
|
let gatewayToolsPrompt = '';
|
|
1285
1302
|
if (this.isGatewayMode && this.useCodeAct) {
|
|
1286
1303
|
baseSystemPrompt = stripTrailingCanonicalCodeActSection(stripGenericGatewayToolsCatalog(baseSystemPrompt));
|
|
@@ -1386,9 +1403,19 @@ class AgentLoop {
|
|
|
1386
1403
|
// That rebuild costs an embedding search, so hand the backend a lazy builder it
|
|
1387
1404
|
// invokes only inside the resume branch; a live thread never pays for it.
|
|
1388
1405
|
const freshSystemPromptBuilder = options?.freshSessionSystemPrompt;
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1406
|
+
// The owner-event and scheduled lanes carry no per-call systemPrompt at all: their
|
|
1407
|
+
// perCallSystemPrompt IS the complete composed owner policy (the ownerRuntime branch
|
|
1408
|
+
// above). Without a resume builder those lanes fell back to the turn-text
|
|
1409
|
+
// <system-reminder> replay - the same full prompt, but billed as USER text on the
|
|
1410
|
+
// first turn after every daemon restart. Re-anchor it through baseInstructions
|
|
1411
|
+
// instead; the reminder is then never needed.
|
|
1412
|
+
const resumeInstructions = !isDurableRuntime
|
|
1413
|
+
? undefined
|
|
1414
|
+
: freshSystemPromptBuilder
|
|
1415
|
+
? async () => prepareSystemPrompt(await freshSystemPromptBuilder(), false, false)
|
|
1416
|
+
: ownerRuntime
|
|
1417
|
+
? async () => perCallSystemPrompt
|
|
1418
|
+
: undefined;
|
|
1392
1419
|
// Reset StopContinuation state for this channel to prevent leaking
|
|
1393
1420
|
// retry counts from previous invocations
|
|
1394
1421
|
if (this.stopContinuationHandler) {
|
|
@@ -1434,6 +1461,12 @@ class AgentLoop {
|
|
|
1434
1461
|
nativeEffects.settled(name, toolUseId, isError);
|
|
1435
1462
|
ext?.onToolComplete?.(name, toolUseId, isError);
|
|
1436
1463
|
},
|
|
1464
|
+
// A spawn is an admission, not an external effect: forwarded verbatim with NO
|
|
1465
|
+
// nativeEffects call, so it never writes a `native_tool` ledger row and never
|
|
1466
|
+
// makes the occurrence unsafe to replay.
|
|
1467
|
+
onSubagentStart: (info) => {
|
|
1468
|
+
ext?.onSubagentStart?.(info);
|
|
1469
|
+
},
|
|
1437
1470
|
onFinal: (finalResponse) => {
|
|
1438
1471
|
ext?.onFinal?.(finalResponse);
|
|
1439
1472
|
},
|
|
@@ -1453,7 +1486,8 @@ class AgentLoop {
|
|
|
1453
1486
|
let requestSystemPrompt = perCallSystemPrompt;
|
|
1454
1487
|
let provisionalDurableSessionId;
|
|
1455
1488
|
// All three backends preserve context and receive only the new user message.
|
|
1456
|
-
const
|
|
1489
|
+
const basePromptText = this.formatLastMessageOnly(history);
|
|
1490
|
+
let promptText = basePromptText;
|
|
1457
1491
|
const promptStart = Date.now();
|
|
1458
1492
|
const throwFinalCliError = (error) => {
|
|
1459
1493
|
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1562,11 +1596,22 @@ class AgentLoop {
|
|
|
1562
1596
|
}
|
|
1563
1597
|
shouldResume = false;
|
|
1564
1598
|
}
|
|
1599
|
+
promptText = this.withProcedureHints(basePromptText, turn, {
|
|
1600
|
+
threadId: resolvedCliSessionId ?? channelKey,
|
|
1601
|
+
// A NEW pool session has no prior thread to carry procedures forward from, even
|
|
1602
|
+
// when the durable runtime is asked to resume: fresh is about the thread.
|
|
1603
|
+
fresh: !shouldResume || sessionIsNew,
|
|
1604
|
+
context: toolExecutionContext,
|
|
1605
|
+
});
|
|
1565
1606
|
piResult = await this.agent.prompt(promptText, callbacks, {
|
|
1566
1607
|
model: options?.model,
|
|
1567
1608
|
resumeSession: shouldResume,
|
|
1568
1609
|
systemPrompt: requestSystemPrompt,
|
|
1569
1610
|
resumeInstructions,
|
|
1611
|
+
promptTelemetry: {
|
|
1612
|
+
kind: options?.promptKind ?? (options?.source === 'operator' ? 'scheduled' : 'chat'),
|
|
1613
|
+
brief: options?.promptBrief ?? 'omitted',
|
|
1614
|
+
},
|
|
1570
1615
|
sessionKey: channelKey,
|
|
1571
1616
|
sessionPolicyFingerprint: effectiveSessionPolicyFingerprint,
|
|
1572
1617
|
sessionId: resolvedCliSessionId ?? undefined,
|
|
@@ -1704,6 +1749,11 @@ class AgentLoop {
|
|
|
1704
1749
|
else if (ownerRuntime) {
|
|
1705
1750
|
resetSystemPrompt = prepareSystemPrompt(options?.systemPrompt, false, true);
|
|
1706
1751
|
}
|
|
1752
|
+
promptText = this.withProcedureHints(basePromptText, turn, {
|
|
1753
|
+
threadId: newSessionId,
|
|
1754
|
+
fresh: true,
|
|
1755
|
+
context: toolExecutionContext,
|
|
1756
|
+
});
|
|
1707
1757
|
piResult = await this.agent.prompt(promptText, callbacks, {
|
|
1708
1758
|
model: options?.model,
|
|
1709
1759
|
resumeSession: false, // Force new session
|
|
@@ -2037,6 +2087,11 @@ class AgentLoop {
|
|
|
2037
2087
|
result.modelRunProvenance = 'commit_failed';
|
|
2038
2088
|
logger.error(`Model run ${ownedModelRunId} may remain uncommitted; provenance reported as commit_failed`);
|
|
2039
2089
|
}
|
|
2090
|
+
if (ownerRuntime && finalResponse.trim()) {
|
|
2091
|
+
// Observation only: a completion claim with no durable write in this run is the
|
|
2092
|
+
// failure the owner sees as "corrected" work that never persisted (2026-09-09).
|
|
2093
|
+
void this.observeCompletionClaim(ownedModelRunId, finalResponse);
|
|
2094
|
+
}
|
|
2040
2095
|
if (ownerRuntime && this.ownerRuntimeJournal && finalResponse.trim()) {
|
|
2041
2096
|
try {
|
|
2042
2097
|
this.ownerRuntimeJournal.append({
|
|
@@ -2076,6 +2131,16 @@ class AgentLoop {
|
|
|
2076
2131
|
throw nativeEffects.failure(error);
|
|
2077
2132
|
}
|
|
2078
2133
|
finally {
|
|
2134
|
+
this.mcpExecutor.releaseProcedureRun?.(toolExecutionContext);
|
|
2135
|
+
// A child that outlives this run still needs its authority recipe, so the context is
|
|
2136
|
+
// dropped only once the run finished AND no child of it is still registered.
|
|
2137
|
+
if (ownedSubagentRunContext) {
|
|
2138
|
+
ownedSubagentRunContext.runFinished = true;
|
|
2139
|
+
if (ownedSubagentRunContext.activeChildren === 0 &&
|
|
2140
|
+
this.subagentRunContexts.get(channelKey) === ownedSubagentRunContext) {
|
|
2141
|
+
this.subagentRunContexts.delete(channelKey);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2079
2144
|
// Always release session lock, even on error
|
|
2080
2145
|
// BUT only if we own the session (not passed by caller)
|
|
2081
2146
|
if (ownedSession) {
|
|
@@ -2126,6 +2191,250 @@ class AgentLoop {
|
|
|
2126
2191
|
},
|
|
2127
2192
|
};
|
|
2128
2193
|
}
|
|
2194
|
+
/** Always above maxTurns: the last-resort stop for a runaway native tool loop. */
|
|
2195
|
+
emergencyMaxCalls() {
|
|
2196
|
+
return Math.max(this.maxTurns + 10, 50);
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* The host tool surface the given options' role grants.
|
|
2200
|
+
*
|
|
2201
|
+
* The Code-Act gate is derived from THOSE options by default, never from the caller's
|
|
2202
|
+
* own role: a child runs under a projected unattended role, so a parent that may hold
|
|
2203
|
+
* outer Code-Act must not hand that marker to a child whose role forbids it.
|
|
2204
|
+
*/
|
|
2205
|
+
hostToolDefinitionsFor(options, outerCodeActAllowed = this.useCodeAct &&
|
|
2206
|
+
roleAllowsOuterCodeAct(options?.agentContext?.role, this.disallowedTools)) {
|
|
2207
|
+
return this.useCodeAct
|
|
2208
|
+
? outerCodeActAllowed
|
|
2209
|
+
? tool_registry_js_1.ToolRegistry.getHostToolDefinitions({ allowedTools: [index_js_1.CODE_ACT_MARKER] })
|
|
2210
|
+
: []
|
|
2211
|
+
: tool_registry_js_1.ToolRegistry.getHostToolDefinitions({
|
|
2212
|
+
allowedTools: options?.agentContext?.role.allowedTools,
|
|
2213
|
+
blockedTools: options?.agentContext?.role.blockedTools,
|
|
2214
|
+
disallowedTools: this.disallowedTools,
|
|
2215
|
+
viewer: options?.agentContext?.platform === 'viewer',
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* One dynamic-tool bridge. Every guard it applies - the emergency call budget and the
|
|
2220
|
+
* same-signature loop guard - is private to the bridge, so a parent and its children
|
|
2221
|
+
* never share a counter. `recordExchange` is the ONLY way a bridge writes to a
|
|
2222
|
+
* conversation: a child passes none and therefore cannot touch the parent's history.
|
|
2223
|
+
*/
|
|
2224
|
+
buildHostToolBridge(params) {
|
|
2225
|
+
let toolCallCount = 0;
|
|
2226
|
+
let consecutiveToolCalls = 0;
|
|
2227
|
+
let lastToolSignature = '';
|
|
2228
|
+
return {
|
|
2229
|
+
tools: params.tools,
|
|
2230
|
+
execute: async (call) => {
|
|
2231
|
+
const callSignal = call.signal ?? new AbortController().signal;
|
|
2232
|
+
callSignal.throwIfAborted();
|
|
2233
|
+
if (toolCallCount >= params.emergencyMaxCalls) {
|
|
2234
|
+
return {
|
|
2235
|
+
content: `Native tool call budget exceeded emergency maximum turns (${params.emergencyMaxCalls})`,
|
|
2236
|
+
isError: true,
|
|
2237
|
+
abort: true,
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
const toolSignature = call.name === index_js_1.CODE_ACT_MARKER && typeof call.input.code === 'string'
|
|
2241
|
+
? `${call.name}:${call.input.code.trim()}`
|
|
2242
|
+
: call.name;
|
|
2243
|
+
const nextConsecutiveCount = toolSignature === lastToolSignature ? consecutiveToolCalls + 1 : 1;
|
|
2244
|
+
if (nextConsecutiveCount >= params.maxConsecutiveSameTool) {
|
|
2245
|
+
return {
|
|
2246
|
+
content: `Infinite loop detected: Tool "${call.name}" called ${nextConsecutiveCount} times consecutively`,
|
|
2247
|
+
isError: true,
|
|
2248
|
+
abort: true,
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
toolCallCount += 1;
|
|
2252
|
+
consecutiveToolCalls = nextConsecutiveCount;
|
|
2253
|
+
lastToolSignature = toolSignature;
|
|
2254
|
+
const toolUse = {
|
|
2255
|
+
type: 'tool_use',
|
|
2256
|
+
id: call.callId,
|
|
2257
|
+
name: call.name,
|
|
2258
|
+
input: call.input,
|
|
2259
|
+
};
|
|
2260
|
+
params.recordExchange?.assistant(toolUse);
|
|
2261
|
+
const executionContext = params.executionContext();
|
|
2262
|
+
const callExecutionContext = executionContext
|
|
2263
|
+
? {
|
|
2264
|
+
...executionContext,
|
|
2265
|
+
gatewayCallId: call.callId,
|
|
2266
|
+
signal: callSignal,
|
|
2267
|
+
}
|
|
2268
|
+
: null;
|
|
2269
|
+
const [toolResult] = await this.executeTools([toolUse], [...params.stopAfterSuccessfulTools], callExecutionContext, params.runScope);
|
|
2270
|
+
if (!toolResult) {
|
|
2271
|
+
callSignal.throwIfAborted();
|
|
2272
|
+
return {
|
|
2273
|
+
content: `Native tool "${call.name}" returned no result`,
|
|
2274
|
+
isError: true,
|
|
2275
|
+
abort: true,
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
if (!toolResult.terminalCode) {
|
|
2279
|
+
callSignal.throwIfAborted();
|
|
2280
|
+
}
|
|
2281
|
+
params.recordExchange?.result(toolResult);
|
|
2282
|
+
return {
|
|
2283
|
+
content: toolResult.content,
|
|
2284
|
+
isError: toolResult.is_error === true,
|
|
2285
|
+
abort: toolResult.abort === true,
|
|
2286
|
+
terminalCode: toolResult.terminalCode,
|
|
2287
|
+
stop: toolResult.is_error !== true && params.stopAfterSuccessfulTools.includes(call.name),
|
|
2288
|
+
};
|
|
2289
|
+
},
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Give one Codex-native child its OWN authority.
|
|
2294
|
+
*
|
|
2295
|
+
* The parent's bridge closes over an envelope issued for the PARENT's wall, which the
|
|
2296
|
+
* child cannot renew - inheriting it meant a long delegated run losing every tool
|
|
2297
|
+
* mid-flight and still reporting "done". So the child gets: its own model run under the
|
|
2298
|
+
* parent's, its own envelope from a host issuer, its own execution context, and its own
|
|
2299
|
+
* bridge counters. When no issuer is reachable it gets nothing and says so - the process
|
|
2300
|
+
* refuses its calls with `subagent authority unavailable` rather than quietly borrowing.
|
|
2301
|
+
*/
|
|
2302
|
+
async createSubagentBridge(info) {
|
|
2303
|
+
const context = this.subagentRunContexts.get(info.sessionKey);
|
|
2304
|
+
if (!context) {
|
|
2305
|
+
console.warn(`[AgentLoop] subagent authority unavailable: no run context for session ${info.sessionKey} ` +
|
|
2306
|
+
`(thread=${info.agentThreadId})`);
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
if (context.runFinished) {
|
|
2310
|
+
// Only a child announced DURING the run inherits that run's issuer. A finished run
|
|
2311
|
+
// stays reachable while a sibling is live; it must not mint authority hours later.
|
|
2312
|
+
console.warn(`[AgentLoop] subagent authority refused: run for session ${info.sessionKey} already ended ` +
|
|
2313
|
+
`(thread=${info.agentThreadId})`);
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
// Claimed synchronously: the parent's finally must not drop the context while this
|
|
2317
|
+
// factory is still awaiting an envelope or a model run.
|
|
2318
|
+
context.activeChildren += 1;
|
|
2319
|
+
const abandon = (reason) => {
|
|
2320
|
+
console.warn(reason);
|
|
2321
|
+
this.releaseSubagentRunContext(info.sessionKey, context);
|
|
2322
|
+
return null;
|
|
2323
|
+
};
|
|
2324
|
+
const issuer = context.options.prepareSubagentEnvelope ?? context.options.prepareEnvelope;
|
|
2325
|
+
if (!issuer) {
|
|
2326
|
+
return abandon(`[AgentLoop] subagent authority unavailable: no host envelope issuer for ${info.sessionKey} ` +
|
|
2327
|
+
`(thread=${info.agentThreadId})`);
|
|
2328
|
+
}
|
|
2329
|
+
let envelope;
|
|
2330
|
+
try {
|
|
2331
|
+
envelope = await issuer();
|
|
2332
|
+
}
|
|
2333
|
+
catch (error) {
|
|
2334
|
+
return abandon(`[AgentLoop] subagent authority issuance failed thread=${info.agentThreadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2335
|
+
}
|
|
2336
|
+
if (!envelope) {
|
|
2337
|
+
return abandon(`[AgentLoop] subagent authority unavailable: issuer returned no envelope ` +
|
|
2338
|
+
`(thread=${info.agentThreadId})`);
|
|
2339
|
+
}
|
|
2340
|
+
const childOptions = {
|
|
2341
|
+
...context.options,
|
|
2342
|
+
envelope,
|
|
2343
|
+
prepareEnvelope: undefined,
|
|
2344
|
+
parentModelRunId: context.parentModelRunId,
|
|
2345
|
+
sourceMessageRef: `subagent:${info.agentThreadId}`,
|
|
2346
|
+
};
|
|
2347
|
+
let modelRunId;
|
|
2348
|
+
try {
|
|
2349
|
+
const modelRun = await this.mcpExecutor.beginRuntimeModelRun(this.buildModelRunInput(childOptions, context.cliSessionId));
|
|
2350
|
+
modelRunId = modelRun.model_run_id;
|
|
2351
|
+
}
|
|
2352
|
+
catch (error) {
|
|
2353
|
+
return abandon(`[AgentLoop] subagent model run could not begin thread=${info.agentThreadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2354
|
+
}
|
|
2355
|
+
const childTasks = [];
|
|
2356
|
+
const backgroundTasks = {
|
|
2357
|
+
register(task) {
|
|
2358
|
+
const observedTask = Promise.resolve(task);
|
|
2359
|
+
observedTask.catch(() => {
|
|
2360
|
+
// Re-thrown later by the child's release drain.
|
|
2361
|
+
});
|
|
2362
|
+
childTasks.push(observedTask);
|
|
2363
|
+
},
|
|
2364
|
+
};
|
|
2365
|
+
let childContext;
|
|
2366
|
+
let bridge;
|
|
2367
|
+
// The child's model run is already OPEN. Anything that throws while assembling its
|
|
2368
|
+
// context or bridge must fail that run and release the parent's context, or the run
|
|
2369
|
+
// stays `running` forever and the context stays pinned by a child that never existed.
|
|
2370
|
+
try {
|
|
2371
|
+
childContext = this.withBackgroundTaskRegistry(this.buildToolExecutionContext({ ...childOptions, modelRunId }), backgroundTasks);
|
|
2372
|
+
if (childContext) {
|
|
2373
|
+
childContext = { ...childContext, subagentThreadId: info.agentThreadId };
|
|
2374
|
+
}
|
|
2375
|
+
// The child's own run scope: no stream callbacks, no onTurn, no onToolUse. Its work
|
|
2376
|
+
// is not this run's turns, and the parent's model run is already committed by then.
|
|
2377
|
+
const runScope = { tier: context.tier, ...(0, temporal_code_act_breaker_js_1.createTemporalCodeActBreakerState)() };
|
|
2378
|
+
bridge = this.buildHostToolBridge({
|
|
2379
|
+
tools: context.tools,
|
|
2380
|
+
runScope,
|
|
2381
|
+
executionContext: () => childContext,
|
|
2382
|
+
stopAfterSuccessfulTools: [],
|
|
2383
|
+
emergencyMaxCalls: this.emergencyMaxCalls(),
|
|
2384
|
+
maxConsecutiveSameTool: MAX_CONSECUTIVE_SAME_HOST_TOOL,
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
catch (error) {
|
|
2388
|
+
const summary = error instanceof Error ? error.message : String(error);
|
|
2389
|
+
try {
|
|
2390
|
+
await this.mcpExecutor.failRuntimeModelRun(modelRunId, `subagent authority could not be assembled: ${summary}`);
|
|
2391
|
+
}
|
|
2392
|
+
catch (failError) {
|
|
2393
|
+
logger.warn(`Failed to mark subagent model run ${modelRunId} failed: ${failError instanceof Error ? failError.message : String(failError)}`);
|
|
2394
|
+
}
|
|
2395
|
+
return abandon(`[AgentLoop] subagent authority could not be assembled thread=${info.agentThreadId}: ${summary}`);
|
|
2396
|
+
}
|
|
2397
|
+
let released = false;
|
|
2398
|
+
return {
|
|
2399
|
+
bridge,
|
|
2400
|
+
release: async (outcome) => {
|
|
2401
|
+
if (released) {
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
released = true;
|
|
2405
|
+
try {
|
|
2406
|
+
await this.drainBackgroundTasks(childTasks);
|
|
2407
|
+
}
|
|
2408
|
+
catch (error) {
|
|
2409
|
+
logger.warn(`AgentLoop subagent background drain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2410
|
+
}
|
|
2411
|
+
try {
|
|
2412
|
+
if (outcome.status === 'completed') {
|
|
2413
|
+
await this.mcpExecutor.commitRuntimeModelRun(modelRunId, `agent_loop subagent ${info.agentPath || info.agentThreadId} completed`);
|
|
2414
|
+
}
|
|
2415
|
+
else {
|
|
2416
|
+
// `unknown` is not success: the run is marked failed with the reason so the
|
|
2417
|
+
// ledger never carries a completion nobody confirmed.
|
|
2418
|
+
await this.mcpExecutor.failRuntimeModelRun(modelRunId, outcome.error ?? `subagent ${outcome.status}`);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
catch (error) {
|
|
2422
|
+
logger.warn(`AgentLoop subagent model run ${modelRunId} could not be closed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2423
|
+
}
|
|
2424
|
+
this.mcpExecutor.releaseProcedureRun?.(childContext);
|
|
2425
|
+
this.releaseSubagentRunContext(info.sessionKey, context);
|
|
2426
|
+
},
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
/** Drop a run's subagent context once the run finished and no child still holds it. */
|
|
2430
|
+
releaseSubagentRunContext(sessionKey, context) {
|
|
2431
|
+
context.activeChildren = Math.max(0, context.activeChildren - 1);
|
|
2432
|
+
if (context.runFinished &&
|
|
2433
|
+
context.activeChildren === 0 &&
|
|
2434
|
+
this.subagentRunContexts.get(sessionKey) === context) {
|
|
2435
|
+
this.subagentRunContexts.delete(sessionKey);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2129
2438
|
/**
|
|
2130
2439
|
* Execute tools from response content blocks
|
|
2131
2440
|
*/
|
|
@@ -2379,6 +2688,39 @@ class AgentLoop {
|
|
|
2379
2688
|
* Format only the last user message for persistent CLI
|
|
2380
2689
|
* Persistent CLI maintains context automatically, so we only send the new message
|
|
2381
2690
|
*/
|
|
2691
|
+
/**
|
|
2692
|
+
* Turn 1 only, once the backend has said whether the thread is live: a fresh or re-opened
|
|
2693
|
+
* thread is told the relevant procedures once, a live thread only what it has not been
|
|
2694
|
+
* told (procedure-runtime.ts). Later turns of a run carry tool results, not hints.
|
|
2695
|
+
*/
|
|
2696
|
+
withProcedureHints(basePromptText, turn, input) {
|
|
2697
|
+
if (turn !== 1)
|
|
2698
|
+
return basePromptText;
|
|
2699
|
+
const hints = this.mcpExecutor.prepareProcedureContext?.(input.context, {
|
|
2700
|
+
threadId: input.threadId,
|
|
2701
|
+
fresh: input.fresh,
|
|
2702
|
+
});
|
|
2703
|
+
// Logged even when empty: with no procedures stored, this is the only sign the
|
|
2704
|
+
// assembler ran and the thread decision it saw.
|
|
2705
|
+
console.log(`[experience] thread=${input.threadId} fresh=${input.fresh} hints=${hints?.hints.length ?? 0}${hints?.hints.length ? `(${hints.hints.join(',')})` : ''} chars=${hints?.text.length ?? 0}`);
|
|
2706
|
+
if (!hints?.text)
|
|
2707
|
+
return basePromptText;
|
|
2708
|
+
return `${hints.text}\n\n${basePromptText}`;
|
|
2709
|
+
}
|
|
2710
|
+
async observeCompletionClaim(modelRunId, finalResponse) {
|
|
2711
|
+
const claim = finalResponse.match(COMPLETION_CLAIM_PATTERN)?.[0];
|
|
2712
|
+
if (!claim || !modelRunId)
|
|
2713
|
+
return;
|
|
2714
|
+
try {
|
|
2715
|
+
const wrote = await this.mcpExecutor.runHadDurableWrite?.(modelRunId);
|
|
2716
|
+
if (wrote === false) {
|
|
2717
|
+
console.warn(`[evidence] completion claim without a durable write: run=${modelRunId} claim="${claim}"`);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
catch (error) {
|
|
2721
|
+
console.warn(`[evidence] claim check unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2382
2724
|
formatLastMessageOnly(history) {
|
|
2383
2725
|
const imageReaderTool = this.backend === 'cline' ? 'read_files' : this.backend === 'codex' ? 'view_image' : 'Read';
|
|
2384
2726
|
// Find the last user message in the history
|