@jungjaehoon/mama-os 0.51.2 → 0.52.1
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 +58 -0
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +503 -142
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
- package/dist/agent/claude-cli-wrapper.js +3 -2
- package/dist/agent/claude-cli-wrapper.js.map +1 -1
- package/dist/agent/cline-cli-adapter.d.ts +2 -0
- package/dist/agent/cline-cli-adapter.d.ts.map +1 -1
- package/dist/agent/cline-cli-adapter.js +2 -0
- package/dist/agent/cline-cli-adapter.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 +16 -0
- package/dist/agent/model-runner.d.ts.map +1 -1
- package/dist/agent/model-runner.js.map +1 -1
- package/dist/agent/persistent-cli-adapter.d.ts +5 -0
- package/dist/agent/persistent-cli-adapter.d.ts.map +1 -1
- package/dist/agent/persistent-cli-adapter.js +7 -0
- package/dist/agent/persistent-cli-adapter.js.map +1 -1
- package/dist/agent/persistent-cli-process.d.ts.map +1 -1
- package/dist/agent/persistent-cli-process.js +27 -2
- package/dist/agent/persistent-cli-process.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 +154 -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/agent-loop-init.d.ts.map +1 -1
- package/dist/cli/runtime/agent-loop-init.js +1 -0
- package/dist/cli/runtime/agent-loop-init.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 +82 -58
- 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 +6 -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/gateways/telegram-response-presenter.d.ts.map +1 -1
- package/dist/gateways/telegram-response-presenter.js +4 -0
- package/dist/gateways/telegram-response-presenter.js.map +1 -1
- package/dist/mcp/code-act-mcp-config.d.ts +49 -0
- package/dist/mcp/code-act-mcp-config.d.ts.map +1 -0
- package/dist/mcp/code-act-mcp-config.js +124 -0
- package/dist/mcp/code-act-mcp-config.js.map +1 -0
- package/dist/multi-agent/runtime-process.d.ts +10 -0
- package/dist/multi-agent/runtime-process.d.ts.map +1 -1
- package/dist/multi-agent/runtime-process.js +8 -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 +13 -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 +106 -14
- package/dist/operator/workorder-consumer.d.ts.map +1 -1
- package/dist/operator/workorder-consumer.js +295 -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 -->';
|
|
@@ -420,11 +407,13 @@ function combineCodeActSessionPolicyFingerprint(callerFingerprint, policy) {
|
|
|
420
407
|
codeActPolicy: policy.fingerprintPayload,
|
|
421
408
|
});
|
|
422
409
|
}
|
|
423
|
-
function ownerRuntimeSessionPolicyFingerprint(role, model) {
|
|
410
|
+
function ownerRuntimeSessionPolicyFingerprint(role, model, supportsNativeSubagents) {
|
|
424
411
|
return JSON.stringify({
|
|
425
412
|
version: 1,
|
|
426
413
|
subject: owner_runtime_js_1.OWNER_RUNTIME_SESSION_KEY,
|
|
427
|
-
|
|
414
|
+
// The fingerprint names the policy the session actually carries: a runner without
|
|
415
|
+
// native subagents is never told to delegate, so it must not be pinned to that text.
|
|
416
|
+
subagentPolicy: supportsNativeSubagents ? owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS : null,
|
|
428
417
|
telegramFormatPolicy: telegram_format_js_1.TELEGRAM_FORMAT_GUIDE,
|
|
429
418
|
model: model ?? null,
|
|
430
419
|
allowedTools: [...(role?.allowedTools ?? [])].sort(),
|
|
@@ -513,6 +502,34 @@ function withExecutionSurface(executionContext, executionSurface) {
|
|
|
513
502
|
executionSurface,
|
|
514
503
|
};
|
|
515
504
|
}
|
|
505
|
+
/** Normal coding tasks often need 10+ consecutive Bash calls; 5 was too low. */
|
|
506
|
+
const MAX_CONSECUTIVE_SAME_HOST_TOOL = 15;
|
|
507
|
+
/**
|
|
508
|
+
* The parent run's shape, kept only so a child announced mid-run (or after it) can be
|
|
509
|
+
* given an authority of its OWN: its own model run under the parent's, its own envelope,
|
|
510
|
+
* its own bridge with its own counters.
|
|
511
|
+
*/
|
|
512
|
+
/**
|
|
513
|
+
* The options a native child of this run inherits. Everything is carried EXCEPT the role,
|
|
514
|
+
* which is projected onto the unattended surface: the owner's 1:1 chat turn holds the
|
|
515
|
+
* gateway shell and file writer, and a child answers to no one inside the turn.
|
|
516
|
+
*/
|
|
517
|
+
function withUnattendedSubagentRole(options) {
|
|
518
|
+
const agentContext = options?.agentContext;
|
|
519
|
+
if (!agentContext) {
|
|
520
|
+
return { ...options };
|
|
521
|
+
}
|
|
522
|
+
const role = (0, owner_event_policy_js_1.projectUnattendedRole)(agentContext.role);
|
|
523
|
+
return {
|
|
524
|
+
...options,
|
|
525
|
+
agentContext: {
|
|
526
|
+
...agentContext,
|
|
527
|
+
role,
|
|
528
|
+
capabilities: [...role.allowedTools],
|
|
529
|
+
limitations: (role.blockedTools ?? []).map((tool) => `Cannot use ${tool}`),
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
516
533
|
class AgentLoop {
|
|
517
534
|
/**
|
|
518
535
|
* Construction-wide child-runtime tool capability.
|
|
@@ -526,6 +543,22 @@ class AgentLoop {
|
|
|
526
543
|
probesDurableSession;
|
|
527
544
|
ownerRecoveryJournalEnabled;
|
|
528
545
|
agent;
|
|
546
|
+
/**
|
|
547
|
+
* Whether the ACTIVE runner can spawn a native subagent the host can observe.
|
|
548
|
+
*
|
|
549
|
+
* Read by the owner prompt path and forwarded to the scheduled work-order contract, so
|
|
550
|
+
* neither has to string-match a backend name. A mocked runner that predates this field
|
|
551
|
+
* is treated as incapable rather than silently promised delegation.
|
|
552
|
+
*/
|
|
553
|
+
get supportsNativeSubagents() {
|
|
554
|
+
return this.agent.supportsNativeSubagents === true;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* What a native child started by a run on this session needs to be given its own
|
|
558
|
+
* authority. Keyed by session key, overwritten by each run on that session, and dropped
|
|
559
|
+
* once the run finished and no child of it is still registered.
|
|
560
|
+
*/
|
|
561
|
+
subagentRunContexts = new Map();
|
|
529
562
|
persistentCLI = null;
|
|
530
563
|
mcpExecutor;
|
|
531
564
|
maxTurns;
|
|
@@ -622,27 +655,12 @@ class AgentLoop {
|
|
|
622
655
|
// Priority 1 (never cut): CLAUDE.md base instructions
|
|
623
656
|
// Priority 2 (cut if extreme): personas (SOUL, IDENTITY, USER) + gateway tools
|
|
624
657
|
// Priority 3 (cut first): context prompt + skills + onboarding
|
|
625
|
-
const mamaHome = (0, path_1.join)((0, os_1.homedir)(), '.mama');
|
|
626
658
|
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);
|
|
659
|
+
const skillCatalog = (0, skill_loader_js_1.filterSkillCatalogForContext)((0, skill_loader_js_1.loadInstalledSkills)(false, {
|
|
660
|
+
includePaths: options.agentContext?.roleName === 'owner_console',
|
|
661
|
+
}), options.agentContext ?? null);
|
|
635
662
|
promptLayers = [
|
|
636
663
|
{ 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
664
|
...(skillCatalog.length > 0
|
|
647
665
|
? [
|
|
648
666
|
{
|
|
@@ -650,7 +668,7 @@ class AgentLoop {
|
|
|
650
668
|
content: [
|
|
651
669
|
'# Installed Skills',
|
|
652
670
|
'',
|
|
653
|
-
'
|
|
671
|
+
'Choose skills by their purpose and the current task, not only literal keywords.',
|
|
654
672
|
'',
|
|
655
673
|
...skillCatalog,
|
|
656
674
|
].join('\n'),
|
|
@@ -750,6 +768,9 @@ class AgentLoop {
|
|
|
750
768
|
mcpConfigPath: this.useCodeAct
|
|
751
769
|
? undefined
|
|
752
770
|
: (options.mcpConfigPath ?? (useMCPMode ? mcpConfigPath : undefined)),
|
|
771
|
+
// A Codex-native child outlives the parent turn. The host - not the child's
|
|
772
|
+
// parent bridge - issues its authority: own model run, own envelope, own counters.
|
|
773
|
+
createSubagentBridge: (info) => this.createSubagentBridge(info),
|
|
753
774
|
});
|
|
754
775
|
logger.debug('Codex app-server backend enabled');
|
|
755
776
|
}
|
|
@@ -792,6 +813,8 @@ class AgentLoop {
|
|
|
792
813
|
// ToolSearch/Agent, native gathering that bypasses report verification).
|
|
793
814
|
// Callers opt in per loop; agent-loop-init locks down the main persona.
|
|
794
815
|
tools: options.builtinTools,
|
|
816
|
+
// agent.effort: the boot gate already proved it is a level this backend accepts.
|
|
817
|
+
effort: options.codexEffort,
|
|
795
818
|
// Pass configured timeout (default in PersistentCLI: 120s — too short for complex tasks)
|
|
796
819
|
requestTimeout: options.timeoutMs,
|
|
797
820
|
});
|
|
@@ -952,6 +975,15 @@ class AgentLoop {
|
|
|
952
975
|
setAgentEventBus(eventBus) {
|
|
953
976
|
this.mcpExecutor.setAgentEventBus(eventBus);
|
|
954
977
|
}
|
|
978
|
+
/**
|
|
979
|
+
* The model runner backing this loop, for host adapters that subscribe to runner
|
|
980
|
+
* lifecycle events (e.g. native subagent completion waking the standing owner).
|
|
981
|
+
* Returned as `unknown` on purpose: the runner surface differs per backend, so the
|
|
982
|
+
* caller must feature-detect rather than assume an emitter.
|
|
983
|
+
*/
|
|
984
|
+
getModelRunner() {
|
|
985
|
+
return this.agent;
|
|
986
|
+
}
|
|
955
987
|
/**
|
|
956
988
|
* Run the agent loop with a user prompt
|
|
957
989
|
*
|
|
@@ -1010,15 +1042,29 @@ class AgentLoop {
|
|
|
1010
1042
|
// Queue wait consumes no execution authority. Never renew a signed envelope implicitly:
|
|
1011
1043
|
// only a host-supplied issuer can prepare the current grant after both lane waits.
|
|
1012
1044
|
if (options?.prepareEnvelope) {
|
|
1045
|
+
const runEnvelopeIssuer = options.prepareEnvelope;
|
|
1013
1046
|
options = {
|
|
1014
1047
|
...options,
|
|
1015
|
-
envelope: await
|
|
1048
|
+
envelope: await runEnvelopeIssuer(),
|
|
1016
1049
|
prepareEnvelope: undefined,
|
|
1050
|
+
// A native child outlives this envelope and needs one of its OWN. The run's issuer
|
|
1051
|
+
// is the fallback when the host supplied no dedicated subagent issuer, so it is kept
|
|
1052
|
+
// reachable here instead of being lost with this rewrite - without it a child would
|
|
1053
|
+
// silently get no authority at all.
|
|
1054
|
+
prepareSubagentEnvelope: options.prepareSubagentEnvelope ?? (async () => await runEnvelopeIssuer()),
|
|
1017
1055
|
};
|
|
1018
1056
|
if (this.stopped) {
|
|
1019
1057
|
throw new types_js_1.AgentError('Agent loop is stopping', 'AGENT_STOPPED', undefined, false);
|
|
1020
1058
|
}
|
|
1021
1059
|
}
|
|
1060
|
+
if (options?.prepareContent) {
|
|
1061
|
+
const admitted = await options.prepareContent();
|
|
1062
|
+
content = admitted.content;
|
|
1063
|
+
options = { ...options, procedureRefs: admitted.procedureRefs, prepareContent: undefined };
|
|
1064
|
+
if (this.stopped) {
|
|
1065
|
+
throw new types_js_1.AgentError('Agent loop is stopping', 'AGENT_STOPPED', undefined, false);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1022
1068
|
if (options?.agentContext) {
|
|
1023
1069
|
options = {
|
|
1024
1070
|
...options,
|
|
@@ -1057,6 +1103,13 @@ class AgentLoop {
|
|
|
1057
1103
|
},
|
|
1058
1104
|
};
|
|
1059
1105
|
let toolExecutionContext = this.withBackgroundTaskRegistry(this.buildToolExecutionContext(options), backgroundTasks);
|
|
1106
|
+
if (toolExecutionContext) {
|
|
1107
|
+
toolExecutionContext = {
|
|
1108
|
+
...toolExecutionContext,
|
|
1109
|
+
procedureStimulus: ownerJournalPrompt,
|
|
1110
|
+
procedureRefs: options?.procedureRefs,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1060
1113
|
// Track this run's tier for code-act execution and prompt sizing.
|
|
1061
1114
|
if (options?.agentContext) {
|
|
1062
1115
|
const rawTier = options.agentContext.tier ?? 1;
|
|
@@ -1069,11 +1122,14 @@ class AgentLoop {
|
|
|
1069
1122
|
// Infinite loop prevention
|
|
1070
1123
|
let consecutiveToolCalls = 0;
|
|
1071
1124
|
let lastToolName = '';
|
|
1072
|
-
const MAX_CONSECUTIVE_SAME_TOOL =
|
|
1073
|
-
const EMERGENCY_MAX_TURNS =
|
|
1125
|
+
const MAX_CONSECUTIVE_SAME_TOOL = MAX_CONSECUTIVE_SAME_HOST_TOOL;
|
|
1126
|
+
const EMERGENCY_MAX_TURNS = this.emergencyMaxCalls(); // Always above maxTurns
|
|
1074
1127
|
// Track channel key for session release
|
|
1075
1128
|
const channelKey = options?.sessionKey ??
|
|
1076
1129
|
(0, session_pool_js_1.buildChannelKey)(options?.source ?? 'default', (0, principal_js_1.laneChannelId)(options?.channelId ?? this.sessionKey, 'owner'));
|
|
1130
|
+
// The context object THIS run created, compared by identity before the finally marks
|
|
1131
|
+
// it finished: a later run on the same session key replaces the map entry.
|
|
1132
|
+
let ownedSubagentRunContext;
|
|
1077
1133
|
// Use session pool for conversation continuity
|
|
1078
1134
|
// IMPORTANT: If caller passes cliSessionId, use it directly to avoid double-locking
|
|
1079
1135
|
// MessageRouter already calls getSession() and passes the result via options
|
|
@@ -1125,7 +1181,7 @@ class AgentLoop {
|
|
|
1125
1181
|
: undefined;
|
|
1126
1182
|
const outerCodeActAllowed = this.useCodeAct && roleAllowsOuterCodeAct(options?.agentContext?.role, this.disallowedTools);
|
|
1127
1183
|
const ownerPolicyFingerprint = ownerRuntime
|
|
1128
|
-
? ownerRuntimeSessionPolicyFingerprint(sessionPolicyRole, options?.model ?? this.model)
|
|
1184
|
+
? ownerRuntimeSessionPolicyFingerprint(sessionPolicyRole, options?.model ?? this.model, this.supportsNativeSubagents)
|
|
1129
1185
|
: undefined;
|
|
1130
1186
|
const effectiveSessionPolicyFingerprint = isDurableRuntime && codeActPolicy
|
|
1131
1187
|
? combineCodeActSessionPolicyFingerprint(ownerPolicyFingerprint ?? options?.sessionPolicyFingerprint, codeActPolicy)
|
|
@@ -1175,112 +1231,92 @@ class AgentLoop {
|
|
|
1175
1231
|
...options,
|
|
1176
1232
|
modelRunId: ownedModelRunId,
|
|
1177
1233
|
}), backgroundTasks);
|
|
1234
|
+
if (toolExecutionContext) {
|
|
1235
|
+
toolExecutionContext = {
|
|
1236
|
+
...toolExecutionContext,
|
|
1237
|
+
procedureStimulus: ownerJournalPrompt,
|
|
1238
|
+
procedureRefs: options?.procedureRefs,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1178
1241
|
}
|
|
1179
1242
|
nativeEffects = new native_effect_observer_js_1.NativeEffectReplayBoundary(this.createNativeEffectObserver?.(toolExecutionContext));
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
let
|
|
1243
|
+
// This run's bridge is ITS OWN: the loop guards, the emergency budget, the history
|
|
1244
|
+
// writes and the turn observers all belong to this run. A Codex-native child gets a
|
|
1245
|
+
// separate bridge from `createSubagentBridge` - sharing this closure let a child trip
|
|
1246
|
+
// the parent's loop guard and append its traces to the parent's committed model run.
|
|
1247
|
+
const hostToolDefinitions = this.hostToolDefinitionsFor(options, outerCodeActAllowed);
|
|
1183
1248
|
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
|
-
});
|
|
1249
|
+
? this.buildHostToolBridge({
|
|
1250
|
+
tools: hostToolDefinitions,
|
|
1251
|
+
runScope,
|
|
1252
|
+
executionContext: () => toolExecutionContext,
|
|
1253
|
+
stopAfterSuccessfulTools: options?.stopAfterSuccessfulTools ?? [],
|
|
1254
|
+
emergencyMaxCalls: EMERGENCY_MAX_TURNS,
|
|
1255
|
+
maxConsecutiveSameTool: MAX_CONSECUTIVE_SAME_TOOL,
|
|
1256
|
+
// Codex does not return completed host exchanges, so record them here. Cline
|
|
1257
|
+
// reports the paired custom-tool exchange from its Hub event stream and
|
|
1258
|
+
// AgentLoop appends it exactly once below.
|
|
1259
|
+
...(isCodex
|
|
1260
|
+
? {
|
|
1261
|
+
recordExchange: {
|
|
1262
|
+
assistant: (toolUse) => {
|
|
1263
|
+
history.push({ role: 'assistant', content: [toolUse] });
|
|
1264
|
+
runScope.onTurn?.({
|
|
1265
|
+
turn,
|
|
1266
|
+
role: 'assistant',
|
|
1267
|
+
content: [toolUse],
|
|
1268
|
+
stopReason: 'tool_use',
|
|
1269
|
+
});
|
|
1270
|
+
},
|
|
1271
|
+
result: (toolResult) => {
|
|
1272
|
+
const persistedToolResult = historyToolResult(toolResult);
|
|
1273
|
+
history.push({ role: 'user', content: [persistedToolResult] });
|
|
1274
|
+
runScope.onTurn?.({
|
|
1275
|
+
turn,
|
|
1276
|
+
role: 'user',
|
|
1277
|
+
content: [persistedToolResult],
|
|
1278
|
+
});
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1264
1281
|
}
|
|
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
|
-
}
|
|
1282
|
+
: {}),
|
|
1283
|
+
})
|
|
1275
1284
|
: undefined;
|
|
1285
|
+
if (hostToolBridge && isCodex) {
|
|
1286
|
+
// A child can be announced during this run and outlive it, so what its authority
|
|
1287
|
+
// needs is recorded now, per session key, and dropped when nothing needs it.
|
|
1288
|
+
// A previous run's context object stays referenced by ITS still-live children, so
|
|
1289
|
+
// this run starts its own count rather than inheriting one it can never settle.
|
|
1290
|
+
// The child NEVER inherits this turn's role verbatim: an owner 1:1 chat turn holds
|
|
1291
|
+
// the gateway shell and file writer, and a child is unattended by definition.
|
|
1292
|
+
const childOptions = withUnattendedSubagentRole(options);
|
|
1293
|
+
ownedSubagentRunContext = {
|
|
1294
|
+
options: childOptions,
|
|
1295
|
+
parentModelRunId: ownedModelRunId ?? options?.modelRunId ?? null,
|
|
1296
|
+
cliSessionId: resolvedCliSessionId,
|
|
1297
|
+
tools: this.hostToolDefinitionsFor(childOptions),
|
|
1298
|
+
tier: runScope.tier,
|
|
1299
|
+
activeChildren: 0,
|
|
1300
|
+
runFinished: false,
|
|
1301
|
+
};
|
|
1302
|
+
this.subagentRunContexts.set(channelKey, ownedSubagentRunContext);
|
|
1303
|
+
}
|
|
1276
1304
|
const prepareSystemPrompt = (requestedSystemPrompt, isResumingSession, includeOwnerRecovery = false) => {
|
|
1277
1305
|
let baseSystemPrompt = requestedSystemPrompt ?? this.defaultSystemPrompt;
|
|
1278
|
-
if (ownerRuntime &&
|
|
1306
|
+
if (ownerRuntime &&
|
|
1307
|
+
this.supportsNativeSubagents &&
|
|
1308
|
+
!baseSystemPrompt.includes(owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS)) {
|
|
1309
|
+
// On a runner with no native subagent (persistent Claude persona, Cline) the
|
|
1310
|
+
// delegation policy asks for something the runtime cannot do. It is omitted, and
|
|
1311
|
+
// nothing replaces it: the turn's own result contract already says what must exist.
|
|
1279
1312
|
baseSystemPrompt = `${baseSystemPrompt}\n\n${owner_runtime_js_1.OWNER_SUBAGENT_INSTRUCTIONS}`;
|
|
1280
1313
|
}
|
|
1281
1314
|
if (ownerRuntime && !baseSystemPrompt.includes(telegram_format_js_1.TELEGRAM_FORMAT_GUIDE)) {
|
|
1282
1315
|
baseSystemPrompt = `${baseSystemPrompt}\n\n${telegram_format_js_1.TELEGRAM_FORMAT_GUIDE}`;
|
|
1283
1316
|
}
|
|
1317
|
+
if (ownerRuntime && !baseSystemPrompt.includes(owner_runtime_js_1.OWNER_RUNTIME_RULES)) {
|
|
1318
|
+
baseSystemPrompt = `${baseSystemPrompt}\n\n${owner_runtime_js_1.OWNER_RUNTIME_RULES}`;
|
|
1319
|
+
}
|
|
1284
1320
|
let gatewayToolsPrompt = '';
|
|
1285
1321
|
if (this.isGatewayMode && this.useCodeAct) {
|
|
1286
1322
|
baseSystemPrompt = stripTrailingCanonicalCodeActSection(stripGenericGatewayToolsCatalog(baseSystemPrompt));
|
|
@@ -1386,9 +1422,19 @@ class AgentLoop {
|
|
|
1386
1422
|
// That rebuild costs an embedding search, so hand the backend a lazy builder it
|
|
1387
1423
|
// invokes only inside the resume branch; a live thread never pays for it.
|
|
1388
1424
|
const freshSystemPromptBuilder = options?.freshSessionSystemPrompt;
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1425
|
+
// The owner-event and scheduled lanes carry no per-call systemPrompt at all: their
|
|
1426
|
+
// perCallSystemPrompt IS the complete composed owner policy (the ownerRuntime branch
|
|
1427
|
+
// above). Without a resume builder those lanes fell back to the turn-text
|
|
1428
|
+
// <system-reminder> replay - the same full prompt, but billed as USER text on the
|
|
1429
|
+
// first turn after every daemon restart. Re-anchor it through baseInstructions
|
|
1430
|
+
// instead; the reminder is then never needed.
|
|
1431
|
+
const resumeInstructions = !isDurableRuntime
|
|
1432
|
+
? undefined
|
|
1433
|
+
: freshSystemPromptBuilder
|
|
1434
|
+
? async () => prepareSystemPrompt(await freshSystemPromptBuilder(), false, false)
|
|
1435
|
+
: ownerRuntime
|
|
1436
|
+
? async () => perCallSystemPrompt
|
|
1437
|
+
: undefined;
|
|
1392
1438
|
// Reset StopContinuation state for this channel to prevent leaking
|
|
1393
1439
|
// retry counts from previous invocations
|
|
1394
1440
|
if (this.stopContinuationHandler) {
|
|
@@ -1434,6 +1480,12 @@ class AgentLoop {
|
|
|
1434
1480
|
nativeEffects.settled(name, toolUseId, isError);
|
|
1435
1481
|
ext?.onToolComplete?.(name, toolUseId, isError);
|
|
1436
1482
|
},
|
|
1483
|
+
// A spawn is an admission, not an external effect: forwarded verbatim with NO
|
|
1484
|
+
// nativeEffects call, so it never writes a `native_tool` ledger row and never
|
|
1485
|
+
// makes the occurrence unsafe to replay.
|
|
1486
|
+
onSubagentStart: (info) => {
|
|
1487
|
+
ext?.onSubagentStart?.(info);
|
|
1488
|
+
},
|
|
1437
1489
|
onFinal: (finalResponse) => {
|
|
1438
1490
|
ext?.onFinal?.(finalResponse);
|
|
1439
1491
|
},
|
|
@@ -1453,7 +1505,8 @@ class AgentLoop {
|
|
|
1453
1505
|
let requestSystemPrompt = perCallSystemPrompt;
|
|
1454
1506
|
let provisionalDurableSessionId;
|
|
1455
1507
|
// All three backends preserve context and receive only the new user message.
|
|
1456
|
-
const
|
|
1508
|
+
const basePromptText = this.formatLastMessageOnly(history);
|
|
1509
|
+
let promptText = basePromptText;
|
|
1457
1510
|
const promptStart = Date.now();
|
|
1458
1511
|
const throwFinalCliError = (error) => {
|
|
1459
1512
|
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1562,11 +1615,22 @@ class AgentLoop {
|
|
|
1562
1615
|
}
|
|
1563
1616
|
shouldResume = false;
|
|
1564
1617
|
}
|
|
1618
|
+
promptText = this.withProcedureHints(basePromptText, turn, {
|
|
1619
|
+
threadId: resolvedCliSessionId ?? channelKey,
|
|
1620
|
+
// A NEW pool session has no prior thread to carry procedures forward from, even
|
|
1621
|
+
// when the durable runtime is asked to resume: fresh is about the thread.
|
|
1622
|
+
fresh: !shouldResume || sessionIsNew,
|
|
1623
|
+
context: toolExecutionContext,
|
|
1624
|
+
});
|
|
1565
1625
|
piResult = await this.agent.prompt(promptText, callbacks, {
|
|
1566
1626
|
model: options?.model,
|
|
1567
1627
|
resumeSession: shouldResume,
|
|
1568
1628
|
systemPrompt: requestSystemPrompt,
|
|
1569
1629
|
resumeInstructions,
|
|
1630
|
+
promptTelemetry: {
|
|
1631
|
+
kind: options?.promptKind ?? (options?.source === 'operator' ? 'scheduled' : 'chat'),
|
|
1632
|
+
brief: options?.promptBrief ?? 'omitted',
|
|
1633
|
+
},
|
|
1570
1634
|
sessionKey: channelKey,
|
|
1571
1635
|
sessionPolicyFingerprint: effectiveSessionPolicyFingerprint,
|
|
1572
1636
|
sessionId: resolvedCliSessionId ?? undefined,
|
|
@@ -1704,6 +1768,11 @@ class AgentLoop {
|
|
|
1704
1768
|
else if (ownerRuntime) {
|
|
1705
1769
|
resetSystemPrompt = prepareSystemPrompt(options?.systemPrompt, false, true);
|
|
1706
1770
|
}
|
|
1771
|
+
promptText = this.withProcedureHints(basePromptText, turn, {
|
|
1772
|
+
threadId: newSessionId,
|
|
1773
|
+
fresh: true,
|
|
1774
|
+
context: toolExecutionContext,
|
|
1775
|
+
});
|
|
1707
1776
|
piResult = await this.agent.prompt(promptText, callbacks, {
|
|
1708
1777
|
model: options?.model,
|
|
1709
1778
|
resumeSession: false, // Force new session
|
|
@@ -2037,6 +2106,11 @@ class AgentLoop {
|
|
|
2037
2106
|
result.modelRunProvenance = 'commit_failed';
|
|
2038
2107
|
logger.error(`Model run ${ownedModelRunId} may remain uncommitted; provenance reported as commit_failed`);
|
|
2039
2108
|
}
|
|
2109
|
+
if (ownerRuntime && finalResponse.trim()) {
|
|
2110
|
+
// Observation only: a completion claim with no durable write in this run is the
|
|
2111
|
+
// failure the owner sees as "corrected" work that never persisted (2026-09-09).
|
|
2112
|
+
void this.observeCompletionClaim(ownedModelRunId, finalResponse);
|
|
2113
|
+
}
|
|
2040
2114
|
if (ownerRuntime && this.ownerRuntimeJournal && finalResponse.trim()) {
|
|
2041
2115
|
try {
|
|
2042
2116
|
this.ownerRuntimeJournal.append({
|
|
@@ -2076,6 +2150,16 @@ class AgentLoop {
|
|
|
2076
2150
|
throw nativeEffects.failure(error);
|
|
2077
2151
|
}
|
|
2078
2152
|
finally {
|
|
2153
|
+
this.mcpExecutor.releaseProcedureRun?.(toolExecutionContext);
|
|
2154
|
+
// A child that outlives this run still needs its authority recipe, so the context is
|
|
2155
|
+
// dropped only once the run finished AND no child of it is still registered.
|
|
2156
|
+
if (ownedSubagentRunContext) {
|
|
2157
|
+
ownedSubagentRunContext.runFinished = true;
|
|
2158
|
+
if (ownedSubagentRunContext.activeChildren === 0 &&
|
|
2159
|
+
this.subagentRunContexts.get(channelKey) === ownedSubagentRunContext) {
|
|
2160
|
+
this.subagentRunContexts.delete(channelKey);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2079
2163
|
// Always release session lock, even on error
|
|
2080
2164
|
// BUT only if we own the session (not passed by caller)
|
|
2081
2165
|
if (ownedSession) {
|
|
@@ -2126,6 +2210,250 @@ class AgentLoop {
|
|
|
2126
2210
|
},
|
|
2127
2211
|
};
|
|
2128
2212
|
}
|
|
2213
|
+
/** Always above maxTurns: the last-resort stop for a runaway native tool loop. */
|
|
2214
|
+
emergencyMaxCalls() {
|
|
2215
|
+
return Math.max(this.maxTurns + 10, 50);
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* The host tool surface the given options' role grants.
|
|
2219
|
+
*
|
|
2220
|
+
* The Code-Act gate is derived from THOSE options by default, never from the caller's
|
|
2221
|
+
* own role: a child runs under a projected unattended role, so a parent that may hold
|
|
2222
|
+
* outer Code-Act must not hand that marker to a child whose role forbids it.
|
|
2223
|
+
*/
|
|
2224
|
+
hostToolDefinitionsFor(options, outerCodeActAllowed = this.useCodeAct &&
|
|
2225
|
+
roleAllowsOuterCodeAct(options?.agentContext?.role, this.disallowedTools)) {
|
|
2226
|
+
return this.useCodeAct
|
|
2227
|
+
? outerCodeActAllowed
|
|
2228
|
+
? tool_registry_js_1.ToolRegistry.getHostToolDefinitions({ allowedTools: [index_js_1.CODE_ACT_MARKER] })
|
|
2229
|
+
: []
|
|
2230
|
+
: tool_registry_js_1.ToolRegistry.getHostToolDefinitions({
|
|
2231
|
+
allowedTools: options?.agentContext?.role.allowedTools,
|
|
2232
|
+
blockedTools: options?.agentContext?.role.blockedTools,
|
|
2233
|
+
disallowedTools: this.disallowedTools,
|
|
2234
|
+
viewer: options?.agentContext?.platform === 'viewer',
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* One dynamic-tool bridge. Every guard it applies - the emergency call budget and the
|
|
2239
|
+
* same-signature loop guard - is private to the bridge, so a parent and its children
|
|
2240
|
+
* never share a counter. `recordExchange` is the ONLY way a bridge writes to a
|
|
2241
|
+
* conversation: a child passes none and therefore cannot touch the parent's history.
|
|
2242
|
+
*/
|
|
2243
|
+
buildHostToolBridge(params) {
|
|
2244
|
+
let toolCallCount = 0;
|
|
2245
|
+
let consecutiveToolCalls = 0;
|
|
2246
|
+
let lastToolSignature = '';
|
|
2247
|
+
return {
|
|
2248
|
+
tools: params.tools,
|
|
2249
|
+
execute: async (call) => {
|
|
2250
|
+
const callSignal = call.signal ?? new AbortController().signal;
|
|
2251
|
+
callSignal.throwIfAborted();
|
|
2252
|
+
if (toolCallCount >= params.emergencyMaxCalls) {
|
|
2253
|
+
return {
|
|
2254
|
+
content: `Native tool call budget exceeded emergency maximum turns (${params.emergencyMaxCalls})`,
|
|
2255
|
+
isError: true,
|
|
2256
|
+
abort: true,
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
const toolSignature = call.name === index_js_1.CODE_ACT_MARKER && typeof call.input.code === 'string'
|
|
2260
|
+
? `${call.name}:${call.input.code.trim()}`
|
|
2261
|
+
: call.name;
|
|
2262
|
+
const nextConsecutiveCount = toolSignature === lastToolSignature ? consecutiveToolCalls + 1 : 1;
|
|
2263
|
+
if (nextConsecutiveCount >= params.maxConsecutiveSameTool) {
|
|
2264
|
+
return {
|
|
2265
|
+
content: `Infinite loop detected: Tool "${call.name}" called ${nextConsecutiveCount} times consecutively`,
|
|
2266
|
+
isError: true,
|
|
2267
|
+
abort: true,
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
toolCallCount += 1;
|
|
2271
|
+
consecutiveToolCalls = nextConsecutiveCount;
|
|
2272
|
+
lastToolSignature = toolSignature;
|
|
2273
|
+
const toolUse = {
|
|
2274
|
+
type: 'tool_use',
|
|
2275
|
+
id: call.callId,
|
|
2276
|
+
name: call.name,
|
|
2277
|
+
input: call.input,
|
|
2278
|
+
};
|
|
2279
|
+
params.recordExchange?.assistant(toolUse);
|
|
2280
|
+
const executionContext = params.executionContext();
|
|
2281
|
+
const callExecutionContext = executionContext
|
|
2282
|
+
? {
|
|
2283
|
+
...executionContext,
|
|
2284
|
+
gatewayCallId: call.callId,
|
|
2285
|
+
signal: callSignal,
|
|
2286
|
+
}
|
|
2287
|
+
: null;
|
|
2288
|
+
const [toolResult] = await this.executeTools([toolUse], [...params.stopAfterSuccessfulTools], callExecutionContext, params.runScope);
|
|
2289
|
+
if (!toolResult) {
|
|
2290
|
+
callSignal.throwIfAborted();
|
|
2291
|
+
return {
|
|
2292
|
+
content: `Native tool "${call.name}" returned no result`,
|
|
2293
|
+
isError: true,
|
|
2294
|
+
abort: true,
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
if (!toolResult.terminalCode) {
|
|
2298
|
+
callSignal.throwIfAborted();
|
|
2299
|
+
}
|
|
2300
|
+
params.recordExchange?.result(toolResult);
|
|
2301
|
+
return {
|
|
2302
|
+
content: toolResult.content,
|
|
2303
|
+
isError: toolResult.is_error === true,
|
|
2304
|
+
abort: toolResult.abort === true,
|
|
2305
|
+
terminalCode: toolResult.terminalCode,
|
|
2306
|
+
stop: toolResult.is_error !== true && params.stopAfterSuccessfulTools.includes(call.name),
|
|
2307
|
+
};
|
|
2308
|
+
},
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Give one Codex-native child its OWN authority.
|
|
2313
|
+
*
|
|
2314
|
+
* The parent's bridge closes over an envelope issued for the PARENT's wall, which the
|
|
2315
|
+
* child cannot renew - inheriting it meant a long delegated run losing every tool
|
|
2316
|
+
* mid-flight and still reporting "done". So the child gets: its own model run under the
|
|
2317
|
+
* parent's, its own envelope from a host issuer, its own execution context, and its own
|
|
2318
|
+
* bridge counters. When no issuer is reachable it gets nothing and says so - the process
|
|
2319
|
+
* refuses its calls with `subagent authority unavailable` rather than quietly borrowing.
|
|
2320
|
+
*/
|
|
2321
|
+
async createSubagentBridge(info) {
|
|
2322
|
+
const context = this.subagentRunContexts.get(info.sessionKey);
|
|
2323
|
+
if (!context) {
|
|
2324
|
+
console.warn(`[AgentLoop] subagent authority unavailable: no run context for session ${info.sessionKey} ` +
|
|
2325
|
+
`(thread=${info.agentThreadId})`);
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
if (context.runFinished) {
|
|
2329
|
+
// Only a child announced DURING the run inherits that run's issuer. A finished run
|
|
2330
|
+
// stays reachable while a sibling is live; it must not mint authority hours later.
|
|
2331
|
+
console.warn(`[AgentLoop] subagent authority refused: run for session ${info.sessionKey} already ended ` +
|
|
2332
|
+
`(thread=${info.agentThreadId})`);
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
// Claimed synchronously: the parent's finally must not drop the context while this
|
|
2336
|
+
// factory is still awaiting an envelope or a model run.
|
|
2337
|
+
context.activeChildren += 1;
|
|
2338
|
+
const abandon = (reason) => {
|
|
2339
|
+
console.warn(reason);
|
|
2340
|
+
this.releaseSubagentRunContext(info.sessionKey, context);
|
|
2341
|
+
return null;
|
|
2342
|
+
};
|
|
2343
|
+
const issuer = context.options.prepareSubagentEnvelope ?? context.options.prepareEnvelope;
|
|
2344
|
+
if (!issuer) {
|
|
2345
|
+
return abandon(`[AgentLoop] subagent authority unavailable: no host envelope issuer for ${info.sessionKey} ` +
|
|
2346
|
+
`(thread=${info.agentThreadId})`);
|
|
2347
|
+
}
|
|
2348
|
+
let envelope;
|
|
2349
|
+
try {
|
|
2350
|
+
envelope = await issuer();
|
|
2351
|
+
}
|
|
2352
|
+
catch (error) {
|
|
2353
|
+
return abandon(`[AgentLoop] subagent authority issuance failed thread=${info.agentThreadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2354
|
+
}
|
|
2355
|
+
if (!envelope) {
|
|
2356
|
+
return abandon(`[AgentLoop] subagent authority unavailable: issuer returned no envelope ` +
|
|
2357
|
+
`(thread=${info.agentThreadId})`);
|
|
2358
|
+
}
|
|
2359
|
+
const childOptions = {
|
|
2360
|
+
...context.options,
|
|
2361
|
+
envelope,
|
|
2362
|
+
prepareEnvelope: undefined,
|
|
2363
|
+
parentModelRunId: context.parentModelRunId,
|
|
2364
|
+
sourceMessageRef: `subagent:${info.agentThreadId}`,
|
|
2365
|
+
};
|
|
2366
|
+
let modelRunId;
|
|
2367
|
+
try {
|
|
2368
|
+
const modelRun = await this.mcpExecutor.beginRuntimeModelRun(this.buildModelRunInput(childOptions, context.cliSessionId));
|
|
2369
|
+
modelRunId = modelRun.model_run_id;
|
|
2370
|
+
}
|
|
2371
|
+
catch (error) {
|
|
2372
|
+
return abandon(`[AgentLoop] subagent model run could not begin thread=${info.agentThreadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2373
|
+
}
|
|
2374
|
+
const childTasks = [];
|
|
2375
|
+
const backgroundTasks = {
|
|
2376
|
+
register(task) {
|
|
2377
|
+
const observedTask = Promise.resolve(task);
|
|
2378
|
+
observedTask.catch(() => {
|
|
2379
|
+
// Re-thrown later by the child's release drain.
|
|
2380
|
+
});
|
|
2381
|
+
childTasks.push(observedTask);
|
|
2382
|
+
},
|
|
2383
|
+
};
|
|
2384
|
+
let childContext;
|
|
2385
|
+
let bridge;
|
|
2386
|
+
// The child's model run is already OPEN. Anything that throws while assembling its
|
|
2387
|
+
// context or bridge must fail that run and release the parent's context, or the run
|
|
2388
|
+
// stays `running` forever and the context stays pinned by a child that never existed.
|
|
2389
|
+
try {
|
|
2390
|
+
childContext = this.withBackgroundTaskRegistry(this.buildToolExecutionContext({ ...childOptions, modelRunId }), backgroundTasks);
|
|
2391
|
+
if (childContext) {
|
|
2392
|
+
childContext = { ...childContext, subagentThreadId: info.agentThreadId };
|
|
2393
|
+
}
|
|
2394
|
+
// The child's own run scope: no stream callbacks, no onTurn, no onToolUse. Its work
|
|
2395
|
+
// is not this run's turns, and the parent's model run is already committed by then.
|
|
2396
|
+
const runScope = { tier: context.tier, ...(0, temporal_code_act_breaker_js_1.createTemporalCodeActBreakerState)() };
|
|
2397
|
+
bridge = this.buildHostToolBridge({
|
|
2398
|
+
tools: context.tools,
|
|
2399
|
+
runScope,
|
|
2400
|
+
executionContext: () => childContext,
|
|
2401
|
+
stopAfterSuccessfulTools: [],
|
|
2402
|
+
emergencyMaxCalls: this.emergencyMaxCalls(),
|
|
2403
|
+
maxConsecutiveSameTool: MAX_CONSECUTIVE_SAME_HOST_TOOL,
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
catch (error) {
|
|
2407
|
+
const summary = error instanceof Error ? error.message : String(error);
|
|
2408
|
+
try {
|
|
2409
|
+
await this.mcpExecutor.failRuntimeModelRun(modelRunId, `subagent authority could not be assembled: ${summary}`);
|
|
2410
|
+
}
|
|
2411
|
+
catch (failError) {
|
|
2412
|
+
logger.warn(`Failed to mark subagent model run ${modelRunId} failed: ${failError instanceof Error ? failError.message : String(failError)}`);
|
|
2413
|
+
}
|
|
2414
|
+
return abandon(`[AgentLoop] subagent authority could not be assembled thread=${info.agentThreadId}: ${summary}`);
|
|
2415
|
+
}
|
|
2416
|
+
let released = false;
|
|
2417
|
+
return {
|
|
2418
|
+
bridge,
|
|
2419
|
+
release: async (outcome) => {
|
|
2420
|
+
if (released) {
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
released = true;
|
|
2424
|
+
try {
|
|
2425
|
+
await this.drainBackgroundTasks(childTasks);
|
|
2426
|
+
}
|
|
2427
|
+
catch (error) {
|
|
2428
|
+
logger.warn(`AgentLoop subagent background drain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2429
|
+
}
|
|
2430
|
+
try {
|
|
2431
|
+
if (outcome.status === 'completed') {
|
|
2432
|
+
await this.mcpExecutor.commitRuntimeModelRun(modelRunId, `agent_loop subagent ${info.agentPath || info.agentThreadId} completed`);
|
|
2433
|
+
}
|
|
2434
|
+
else {
|
|
2435
|
+
// `unknown` is not success: the run is marked failed with the reason so the
|
|
2436
|
+
// ledger never carries a completion nobody confirmed.
|
|
2437
|
+
await this.mcpExecutor.failRuntimeModelRun(modelRunId, outcome.error ?? `subagent ${outcome.status}`);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
catch (error) {
|
|
2441
|
+
logger.warn(`AgentLoop subagent model run ${modelRunId} could not be closed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2442
|
+
}
|
|
2443
|
+
this.mcpExecutor.releaseProcedureRun?.(childContext);
|
|
2444
|
+
this.releaseSubagentRunContext(info.sessionKey, context);
|
|
2445
|
+
},
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
/** Drop a run's subagent context once the run finished and no child still holds it. */
|
|
2449
|
+
releaseSubagentRunContext(sessionKey, context) {
|
|
2450
|
+
context.activeChildren = Math.max(0, context.activeChildren - 1);
|
|
2451
|
+
if (context.runFinished &&
|
|
2452
|
+
context.activeChildren === 0 &&
|
|
2453
|
+
this.subagentRunContexts.get(sessionKey) === context) {
|
|
2454
|
+
this.subagentRunContexts.delete(sessionKey);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2129
2457
|
/**
|
|
2130
2458
|
* Execute tools from response content blocks
|
|
2131
2459
|
*/
|
|
@@ -2379,6 +2707,39 @@ class AgentLoop {
|
|
|
2379
2707
|
* Format only the last user message for persistent CLI
|
|
2380
2708
|
* Persistent CLI maintains context automatically, so we only send the new message
|
|
2381
2709
|
*/
|
|
2710
|
+
/**
|
|
2711
|
+
* Turn 1 only, once the backend has said whether the thread is live: a fresh or re-opened
|
|
2712
|
+
* thread is told the relevant procedures once, a live thread only what it has not been
|
|
2713
|
+
* told (procedure-runtime.ts). Later turns of a run carry tool results, not hints.
|
|
2714
|
+
*/
|
|
2715
|
+
withProcedureHints(basePromptText, turn, input) {
|
|
2716
|
+
if (turn !== 1)
|
|
2717
|
+
return basePromptText;
|
|
2718
|
+
const hints = this.mcpExecutor.prepareProcedureContext?.(input.context, {
|
|
2719
|
+
threadId: input.threadId,
|
|
2720
|
+
fresh: input.fresh,
|
|
2721
|
+
});
|
|
2722
|
+
// Logged even when empty: with no procedures stored, this is the only sign the
|
|
2723
|
+
// assembler ran and the thread decision it saw.
|
|
2724
|
+
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}`);
|
|
2725
|
+
if (!hints?.text)
|
|
2726
|
+
return basePromptText;
|
|
2727
|
+
return `${hints.text}\n\n${basePromptText}`;
|
|
2728
|
+
}
|
|
2729
|
+
async observeCompletionClaim(modelRunId, finalResponse) {
|
|
2730
|
+
const claim = finalResponse.match(COMPLETION_CLAIM_PATTERN)?.[0];
|
|
2731
|
+
if (!claim || !modelRunId)
|
|
2732
|
+
return;
|
|
2733
|
+
try {
|
|
2734
|
+
const wrote = await this.mcpExecutor.runHadDurableWrite?.(modelRunId);
|
|
2735
|
+
if (wrote === false) {
|
|
2736
|
+
console.warn(`[evidence] completion claim without a durable write: run=${modelRunId} claim="${claim}"`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
catch (error) {
|
|
2740
|
+
console.warn(`[evidence] claim check unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2382
2743
|
formatLastMessageOnly(history) {
|
|
2383
2744
|
const imageReaderTool = this.backend === 'cline' ? 'read_files' : this.backend === 'codex' ? 'view_image' : 'Read';
|
|
2384
2745
|
// Find the last user message in the history
|