comisai 1.0.25 → 1.0.27

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.
Files changed (145) hide show
  1. package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +130 -10
  2. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +1 -1
  3. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +9 -2
  4. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +8 -0
  5. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +2 -0
  6. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +29 -0
  7. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +242 -2
  8. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.d.ts +210 -0
  9. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.js +566 -0
  10. package/node_modules/@comis/agent/dist/context-engine/context-engine.js +8 -6
  11. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -30
  12. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +109 -36
  13. package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +5 -1
  14. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +22 -20
  15. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +2 -0
  16. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +111 -15
  17. package/node_modules/@comis/agent/dist/executor/executor-response-filter.d.ts +20 -17
  18. package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +132 -52
  19. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +16 -3
  20. package/node_modules/@comis/agent/dist/executor/model-retry.d.ts +14 -0
  21. package/node_modules/@comis/agent/dist/executor/model-retry.js +72 -1
  22. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +3 -0
  23. package/node_modules/@comis/agent/dist/executor/pi-executor.js +68 -9
  24. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.d.ts +82 -0
  25. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.js +200 -0
  26. package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +1 -9
  27. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +37 -2
  28. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +45 -3
  29. package/node_modules/@comis/agent/dist/executor/tool-parallelism.js +0 -1
  30. package/node_modules/@comis/agent/dist/executor/types.d.ts +11 -2
  31. package/node_modules/@comis/agent/dist/index.d.ts +3 -1
  32. package/node_modules/@comis/agent/dist/index.js +2 -0
  33. package/node_modules/@comis/agent/dist/model/last-known-model.d.ts +36 -0
  34. package/node_modules/@comis/agent/dist/model/last-known-model.js +49 -0
  35. package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +16 -4
  36. package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +65 -21
  37. package/node_modules/@comis/agent/dist/planner/types.d.ts +0 -2
  38. package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +10 -0
  39. package/node_modules/@comis/agent/dist/session/comis-session-manager.js +5 -0
  40. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +7 -0
  41. package/node_modules/@comis/agent/package.json +1 -1
  42. package/node_modules/@comis/channels/package.json +1 -1
  43. package/node_modules/@comis/cli/dist/client/rpc-client.js +6 -1
  44. package/node_modules/@comis/cli/dist/commands/doctor.js +5 -3
  45. package/node_modules/@comis/cli/dist/commands/health.js +5 -2
  46. package/node_modules/@comis/cli/dist/wizard/json-output.js +7 -3
  47. package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +130 -0
  48. package/node_modules/@comis/cli/package.json +1 -1
  49. package/node_modules/@comis/core/dist/config/immutable-keys.d.ts +2 -2
  50. package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
  51. package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
  52. package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
  53. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
  54. package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
  55. package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
  56. package/node_modules/@comis/core/dist/config/schema.js +14 -0
  57. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
  58. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
  59. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  60. package/node_modules/@comis/core/dist/exports/config.js +1 -1
  61. package/node_modules/@comis/core/package.json +1 -1
  62. package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
  63. package/node_modules/@comis/daemon/dist/daemon.js +42 -0
  64. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
  65. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
  66. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
  67. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
  68. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
  69. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
  70. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
  71. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
  72. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
  73. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
  74. package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
  75. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
  76. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
  77. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
  78. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
  79. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
  80. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
  81. package/node_modules/@comis/daemon/package.json +1 -1
  82. package/node_modules/@comis/gateway/package.json +1 -1
  83. package/node_modules/@comis/infra/dist/index.d.ts +1 -0
  84. package/node_modules/@comis/infra/dist/index.js +2 -0
  85. package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
  86. package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
  87. package/node_modules/@comis/infra/package.json +1 -1
  88. package/node_modules/@comis/memory/package.json +1 -1
  89. package/node_modules/@comis/scheduler/package.json +1 -1
  90. package/node_modules/@comis/shared/package.json +1 -1
  91. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
  92. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
  93. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
  94. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
  95. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
  96. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
  97. package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
  98. package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
  99. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
  100. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
  101. package/node_modules/@comis/skills/dist/index.d.ts +1 -1
  102. package/node_modules/@comis/skills/dist/index.js +2 -2
  103. package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
  104. package/node_modules/@comis/skills/package.json +1 -1
  105. package/node_modules/@comis/web/dist/assets/{agent-detail-ru-AhppM.js → agent-detail-DqL6Artv.js} +1 -1
  106. package/node_modules/@comis/web/dist/assets/{agent-editor-hjwRuFVp.js → agent-editor-CNM_h94Y.js} +1 -1
  107. package/node_modules/@comis/web/dist/assets/{agent-list-6Uotjatr.js → agent-list-Dbh-xD_F.js} +1 -1
  108. package/node_modules/@comis/web/dist/assets/{billing-view-CxysXH0p.js → billing-view-C1DmtyzK.js} +1 -1
  109. package/node_modules/@comis/web/dist/assets/{channel-detail-BBCKtmne.js → channel-detail-CtCH22N1.js} +1 -1
  110. package/node_modules/@comis/web/dist/assets/{channel-list-FkfeOLBQ.js → channel-list-C7xXn-60.js} +1 -1
  111. package/node_modules/@comis/web/dist/assets/{chat-console-BumBaIgO.js → chat-console-C51pjFwk.js} +1 -1
  112. package/node_modules/@comis/web/dist/assets/{config-editor-C9BSwHGy.js → config-editor-BLArYRB7.js} +1 -1
  113. package/node_modules/@comis/web/dist/assets/{context-dag-browser-BHm00mJD.js → context-dag-browser-fuyMinNI.js} +1 -1
  114. package/node_modules/@comis/web/dist/assets/{context-engine-BENY3pWE.js → context-engine-Bngf2bH0.js} +1 -1
  115. package/node_modules/@comis/web/dist/assets/{delivery-view-BCnkPsAp.js → delivery-view-C80hucxX.js} +1 -1
  116. package/node_modules/@comis/web/dist/assets/{diagnostics-view-C_jQFG2H.js → diagnostics-view-Cl4VbHZ6.js} +1 -1
  117. package/node_modules/@comis/web/dist/assets/{ic-chat-message-FdQcZsSQ.js → ic-chat-message-ByFUoMm6.js} +1 -1
  118. package/node_modules/@comis/web/dist/assets/{ic-connection-dot-BgYiK2N4.js → ic-connection-dot-C4nDHgY2.js} +1 -1
  119. package/node_modules/@comis/web/dist/assets/{ic-tool-call-DMPHsLyx.js → ic-tool-call-Bh5kq-yY.js} +1 -1
  120. package/node_modules/@comis/web/dist/assets/{index-FLPhHz8p.js → index-BBkuC-EU.js} +2 -2
  121. package/node_modules/@comis/web/dist/assets/{mcp-management-5jyScQis.js → mcp-management-DB-phOo7.js} +1 -1
  122. package/node_modules/@comis/web/dist/assets/{media-config-J9oT9PPs.js → media-config-CRqZ1ZUH.js} +1 -1
  123. package/node_modules/@comis/web/dist/assets/{media-test-DGTCtM8-.js → media-test-C9vE20Oy.js} +1 -1
  124. package/node_modules/@comis/web/dist/assets/{memory-inspector-D5Re9ptG.js → memory-inspector-CeqfnxMZ.js} +1 -1
  125. package/node_modules/@comis/web/dist/assets/{message-center-cRLK6ZmG.js → message-center-Daup7Mof.js} +1 -1
  126. package/node_modules/@comis/web/dist/assets/{models-D5vu07MR.js → models-DLYnEU8E.js} +1 -1
  127. package/node_modules/@comis/web/dist/assets/{observe-view-CalNNEmd.js → observe-view-BTSt_PO5.js} +1 -1
  128. package/node_modules/@comis/web/dist/assets/{pipeline-builder-DUYDGwZf.js → pipeline-builder-DknfzyLt.js} +1 -1
  129. package/node_modules/@comis/web/dist/assets/{pipeline-history-BAO8brOe.js → pipeline-history-JnHZdeU_.js} +1 -1
  130. package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-DectIoQt.js → pipeline-history-detail-Dg4knsEb.js} +1 -1
  131. package/node_modules/@comis/web/dist/assets/{pipeline-list-BHlaBKww.js → pipeline-list-AEnibjsp.js} +1 -1
  132. package/node_modules/@comis/web/dist/assets/{pipeline-monitor-BhtpNEHf.js → pipeline-monitor-DG7RbIOO.js} +1 -1
  133. package/node_modules/@comis/web/dist/assets/{scheduler-VafN_8xi.js → scheduler-uL1fYKAT.js} +1 -1
  134. package/node_modules/@comis/web/dist/assets/{security-QQXMRTlo.js → security-C3DywRLH.js} +1 -1
  135. package/node_modules/@comis/web/dist/assets/{session-detail-BpZ_8Yih.js → session-detail-BtqCNWXV.js} +1 -1
  136. package/node_modules/@comis/web/dist/assets/{session-list-DfCm8Cec.js → session-list-CJXWa2XT.js} +1 -1
  137. package/node_modules/@comis/web/dist/assets/{setup-wizard-C-z477CG.js → setup-wizard-ywn7oJvu.js} +1 -1
  138. package/node_modules/@comis/web/dist/assets/{skills-BCOGPf6s.js → skills-DX0KYnWD.js} +1 -1
  139. package/node_modules/@comis/web/dist/assets/{subagents-l-auUraL.js → subagents-B8p5YJEB.js} +1 -1
  140. package/node_modules/@comis/web/dist/assets/{workspace-manager-DlvBixiq.js → workspace-manager-CgzNIrw1.js} +1 -1
  141. package/node_modules/@comis/web/dist/index.html +1 -1
  142. package/node_modules/@comis/web/package.json +1 -1
  143. package/package.json +14 -14
  144. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
  145. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.js +0 -39
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Post-batch continuation handler (L4 silent-termination recovery).
3
+ *
4
+ * When the LLM emits an empty final assistant turn (zero text + zero thinking +
5
+ * zero tool calls) following a successful tool batch within the same execution
6
+ * window, this handler fires a directive `session.followUp()` with multi-shot
7
+ * retry. Replaces the legacy SEP one-shot `generateCompletenessNudge` (whose
8
+ * enforcement role is now superseded; SEP plan extraction + step counting
9
+ * remain intact for observability).
10
+ *
11
+ * @module
12
+ */
13
+ import { type Result } from "@comis/shared";
14
+ import type { ComisLogger } from "@comis/infra";
15
+ /** Configuration for the post-batch continuation handler. */
16
+ export interface ContinuationConfig {
17
+ /** Master toggle. When false, handler returns
18
+ * `{recovered: false, outcome: "disabled"}` without calling followUp. */
19
+ enabled: boolean;
20
+ /** Maximum directive followUp attempts before falling through to L3
21
+ * synthesis. Range 0..5; 0 is treated as disabled. */
22
+ maxRetries: number;
23
+ }
24
+ /** Outcome returned by the handler. */
25
+ export interface ContinuationOutcome {
26
+ recovered: boolean;
27
+ /** Recovered visible text from the followed-up assistant turn (only set
28
+ * when `recovered === true`). */
29
+ response?: string;
30
+ /** Number of followUp attempts actually made (0 when handler did not fire). */
31
+ attempts: number;
32
+ /** Terminal outcome:
33
+ * - `recovered` — followUp produced visible text on some attempt
34
+ * - `still_empty` — followUp ran but produced no visible text
35
+ * (single-attempt diagnostic; not a terminal flag)
36
+ * - `max_attempts_exhausted` — all `maxRetries` attempts produced empty
37
+ * - `disabled` — config.enabled = false OR maxRetries = 0
38
+ * - `no_match` — empty-after-tool-batch pattern not detected */
39
+ outcome: "recovered" | "still_empty" | "max_attempts_exhausted" | "disabled" | "no_match";
40
+ priorToolCallCount: number;
41
+ priorToolNames: string[];
42
+ }
43
+ /** Error variant — only ever returned when `session.followUp` rejects. */
44
+ export type ContinuationError = {
45
+ kind: "followup_error";
46
+ cause: unknown;
47
+ };
48
+ /** Dependencies passed in by the executor wire-in site. */
49
+ export interface RunPostBatchContinuationDeps {
50
+ /** Live session — invoked via `followUp(text)` to issue the directive. */
51
+ session: {
52
+ followUp(text: string): Promise<unknown>;
53
+ messages?: unknown[];
54
+ };
55
+ /** Session messages — passed explicitly per the canonical
56
+ * `(session as any).messages ?? []` pattern at executor-prompt-runner.ts:797. */
57
+ messages: unknown[];
58
+ config: ContinuationConfig;
59
+ logger: ComisLogger;
60
+ agentId?: string;
61
+ /** Read visible text from the latest assistant turn (post-followUp). */
62
+ getVisibleAssistantText: (session: any) => string;
63
+ }
64
+ /**
65
+ * Run the post-batch continuation handler. Returns a `Result` so callers can
66
+ * distinguish a clean outcome (any `ContinuationOutcome.outcome` value) from
67
+ * a true error (followUp rejected).
68
+ *
69
+ * Detection (pure inspection, no throw):
70
+ * 1. Walk `messages` to find the most recent user-role index (lower bound).
71
+ * 2. The last message must be assistant with NO visible text, NO thinking
72
+ * blocks, and NO tool_use/toolCall blocks.
73
+ * 3. Within `[lowerBound, messages.length)`, count assistant turns whose
74
+ * content includes tool_use/toolCall blocks; collect tool names where
75
+ * `block.name` is a string.
76
+ * 4. Fire when (2) AND (≥1 tool call from step 3); else `no_match`.
77
+ *
78
+ * `session.followUp` errors are caught and propagated as
79
+ * `Result<_, ContinuationError>` per AGENTS.md §2.1 + the
80
+ * `executor-prompt-runner.ts:931` precedent.
81
+ */
82
+ export declare function runPostBatchContinuation(deps: RunPostBatchContinuationDeps): Promise<Result<ContinuationOutcome, ContinuationError>>;
@@ -0,0 +1,200 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Post-batch continuation handler (L4 silent-termination recovery).
4
+ *
5
+ * When the LLM emits an empty final assistant turn (zero text + zero thinking +
6
+ * zero tool calls) following a successful tool batch within the same execution
7
+ * window, this handler fires a directive `session.followUp()` with multi-shot
8
+ * retry. Replaces the legacy SEP one-shot `generateCompletenessNudge` (whose
9
+ * enforcement role is now superseded; SEP plan extraction + step counting
10
+ * remain intact for observability).
11
+ *
12
+ * @module
13
+ */
14
+ import { fromPromise, ok, err } from "@comis/shared";
15
+ // ---------------------------------------------------------------------------
16
+ // Implementation
17
+ // ---------------------------------------------------------------------------
18
+ const MODULE = "agent.executor.post-batch-continuation";
19
+ /* eslint-disable @typescript-eslint/no-explicit-any */
20
+ function isToolCallBlock(block) {
21
+ return block?.type === "toolCall" || block?.type === "tool_use";
22
+ }
23
+ function isThinkingBlock(block) {
24
+ return block?.type === "thinking";
25
+ }
26
+ function hasVisibleTextBlock(content) {
27
+ if (!Array.isArray(content))
28
+ return false;
29
+ for (const block of content) {
30
+ if (block?.type === "text" &&
31
+ typeof block.text === "string" &&
32
+ block.text.trim().length > 0) {
33
+ return true;
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+ function hasThinkingBlock(content) {
39
+ if (!Array.isArray(content))
40
+ return false;
41
+ return content.some(isThinkingBlock);
42
+ }
43
+ function hasToolCallBlock(content) {
44
+ if (!Array.isArray(content))
45
+ return false;
46
+ return content.some(isToolCallBlock);
47
+ }
48
+ function findLastUserIndex(messages) {
49
+ for (let i = messages.length - 1; i >= 0; i--) {
50
+ if (messages[i]?.role === "user")
51
+ return i; // eslint-disable-line security/detect-object-injection
52
+ }
53
+ return 0;
54
+ }
55
+ function buildDirective(priorToolCallCount, priorToolNames) {
56
+ const toolList = priorToolNames.join(", ");
57
+ return (`[comis: post-batch continuation — your last turn was empty after ${priorToolCallCount} successful tool calls]\n` +
58
+ `You completed ${priorToolCallCount} tool calls (toolNames: [${toolList}]). Your previous turn produced no text, thinking, or new tool calls. The conversation is incomplete.\n\n` +
59
+ `You MUST either:\n` +
60
+ ` (a) Provide a brief summary of what you accomplished AND continue with the next step from your plan, OR\n` +
61
+ ` (b) Explicitly state "task complete" with reasoning for stopping (e.g., "All N agents created and ROLE.md customized — the user can now use them").\n\n` +
62
+ `Do NOT emit empty turns. If you have nothing else to do, say so explicitly.`);
63
+ }
64
+ /* eslint-enable @typescript-eslint/no-explicit-any */
65
+ /**
66
+ * Run the post-batch continuation handler. Returns a `Result` so callers can
67
+ * distinguish a clean outcome (any `ContinuationOutcome.outcome` value) from
68
+ * a true error (followUp rejected).
69
+ *
70
+ * Detection (pure inspection, no throw):
71
+ * 1. Walk `messages` to find the most recent user-role index (lower bound).
72
+ * 2. The last message must be assistant with NO visible text, NO thinking
73
+ * blocks, and NO tool_use/toolCall blocks.
74
+ * 3. Within `[lowerBound, messages.length)`, count assistant turns whose
75
+ * content includes tool_use/toolCall blocks; collect tool names where
76
+ * `block.name` is a string.
77
+ * 4. Fire when (2) AND (≥1 tool call from step 3); else `no_match`.
78
+ *
79
+ * `session.followUp` errors are caught and propagated as
80
+ * `Result<_, ContinuationError>` per AGENTS.md §2.1 + the
81
+ * `executor-prompt-runner.ts:931` precedent.
82
+ */
83
+ export async function runPostBatchContinuation(deps) {
84
+ const { session, messages, config, logger, agentId, getVisibleAssistantText } = deps;
85
+ // Step 1: disable check.
86
+ if (!config.enabled || config.maxRetries === 0) {
87
+ logger.info({ module: MODULE, agentId, decision: "skip", reason: "disabled" }, "Post-batch continuation skipped");
88
+ return ok({
89
+ recovered: false,
90
+ attempts: 0,
91
+ outcome: "disabled",
92
+ priorToolCallCount: 0,
93
+ priorToolNames: [],
94
+ });
95
+ }
96
+ // Step 2: detection — last message must be empty assistant turn.
97
+ /* eslint-disable @typescript-eslint/no-explicit-any */
98
+ const msgs = messages;
99
+ if (!Array.isArray(msgs) || msgs.length === 0) {
100
+ logger.info({ module: MODULE, agentId, decision: "skip", reason: "non_empty_final" }, "Post-batch continuation skipped");
101
+ return ok({
102
+ recovered: false,
103
+ attempts: 0,
104
+ outcome: "no_match",
105
+ priorToolCallCount: 0,
106
+ priorToolNames: [],
107
+ });
108
+ }
109
+ const last = msgs[msgs.length - 1];
110
+ const lastIsAssistant = last?.role === "assistant";
111
+ const lastIsEmpty = lastIsAssistant &&
112
+ !hasVisibleTextBlock(last.content) &&
113
+ !hasThinkingBlock(last.content) &&
114
+ !hasToolCallBlock(last.content);
115
+ if (!lastIsEmpty) {
116
+ logger.info({ module: MODULE, agentId, decision: "skip", reason: "non_empty_final" }, "Post-batch continuation skipped");
117
+ return ok({
118
+ recovered: false,
119
+ attempts: 0,
120
+ outcome: "no_match",
121
+ priorToolCallCount: 0,
122
+ priorToolNames: [],
123
+ });
124
+ }
125
+ // Step 3: collect tool calls within the current execution window.
126
+ const lowerBound = findLastUserIndex(msgs);
127
+ let priorToolCallCount = 0;
128
+ const priorToolNamesSet = new Set();
129
+ for (let i = lowerBound; i < msgs.length; i++) {
130
+ const m = msgs[i]; // eslint-disable-line security/detect-object-injection
131
+ if (m?.role !== "assistant" || !Array.isArray(m.content))
132
+ continue;
133
+ for (const block of m.content) {
134
+ if (isToolCallBlock(block)) {
135
+ priorToolCallCount++;
136
+ if (typeof block?.name === "string")
137
+ priorToolNamesSet.add(block.name);
138
+ }
139
+ }
140
+ }
141
+ /* eslint-enable @typescript-eslint/no-explicit-any */
142
+ const priorToolNames = [...priorToolNamesSet];
143
+ if (priorToolCallCount === 0) {
144
+ logger.info({ module: MODULE, agentId, decision: "skip", reason: "no_tool_calls" }, "Post-batch continuation skipped");
145
+ return ok({
146
+ recovered: false,
147
+ attempts: 0,
148
+ outcome: "no_match",
149
+ priorToolCallCount: 0,
150
+ priorToolNames: [],
151
+ });
152
+ }
153
+ // Step 4: decision-log fire.
154
+ logger.info({
155
+ module: MODULE,
156
+ agentId,
157
+ decision: "fire",
158
+ reason: "empty_after_tool_batch",
159
+ priorToolCallCount,
160
+ priorToolNames,
161
+ maxAttempts: config.maxRetries,
162
+ }, "Post-batch continuation firing");
163
+ // Step 5: directive multi-shot retry loop.
164
+ const directive = buildDirective(priorToolCallCount, priorToolNames);
165
+ for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
166
+ const followUpResult = await fromPromise(session.followUp(directive));
167
+ if (!followUpResult.ok) {
168
+ return err({ kind: "followup_error", cause: followUpResult.error });
169
+ }
170
+ const text = getVisibleAssistantText(session);
171
+ const outcomeForLog = text && text.length > 0 ? "recovered" : "still_empty";
172
+ logger.info({
173
+ module: MODULE,
174
+ agentId,
175
+ attempt,
176
+ maxAttempts: config.maxRetries,
177
+ priorToolCallCount,
178
+ priorToolNames,
179
+ outcome: outcomeForLog,
180
+ }, "Post-batch continuation attempt");
181
+ if (text && text.length > 0) {
182
+ return ok({
183
+ recovered: true,
184
+ response: text,
185
+ attempts: attempt,
186
+ outcome: "recovered",
187
+ priorToolCallCount,
188
+ priorToolNames,
189
+ });
190
+ }
191
+ }
192
+ // Step 6: max retries exhausted.
193
+ return ok({
194
+ recovered: false,
195
+ attempts: config.maxRetries,
196
+ outcome: "max_attempts_exhausted",
197
+ priorToolCallCount,
198
+ priorToolNames,
199
+ });
200
+ }
@@ -17,6 +17,7 @@ import { createAccumulativeLatch } from "../session-latch.js";
17
17
  import { MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, CACHE_LOOKBACK_WINDOW } from "../../context-engine/index.js";
18
18
  import { estimateContextChars } from "../../safety/token-estimator.js";
19
19
  import { computeHash, djb2 } from "../cache-break-detection.js";
20
+ import { supportsToolSearch } from "../tool-deferral.js";
20
21
  // ---------------------------------------------------------------------------
21
22
  // Tool schema caches extracted to tool-schema-cache.ts (leaf module).
22
23
  // Re-exported here for backward compatibility with existing consumers.
@@ -208,15 +209,6 @@ export function addCacheControlToLastBlock(message, retention) {
208
209
  // Edge case: no cacheable block found -- place on last block as fallback
209
210
  content[content.length - 1].cache_control = cacheControl;
210
211
  }
211
- /** DEFER-TOOL: Check if a model supports Anthropic's tool search (defer_loading).
212
- * Supported: Sonnet 4.x+, Opus 4.x+. NOT supported: Haiku. */
213
- function supportsToolSearch(modelId) {
214
- const lower = modelId.toLowerCase();
215
- if (lower.includes("haiku"))
216
- return false;
217
- // Sonnet 4.x and Opus 4.x support tool search
218
- return lower.includes("sonnet") || lower.includes("opus");
219
- }
220
212
  /**
221
213
  * Place exactly 1 cache_control marker on the second-to-last user message.
222
214
  * The SDK already places one on the last user message, so we target second-to-last
@@ -100,6 +100,20 @@ export declare function resolveModelTier(contextWindow: number): ModelTier;
100
100
  * Small models benefit from deterministic tool selection (0.0).
101
101
  */
102
102
  export declare function resolveToolCallingTemperature(modelTier: ModelTier): number;
103
+ /**
104
+ * Anthropic models that support server-side tool_search_tool_regex
105
+ * (defer_loading). Sonnet 4.x+, Opus 4.x+; NOT Haiku.
106
+ *
107
+ * When this returns true, request-body-injector strips client-side
108
+ * `discover_tools` from the API payload and appends `tool_search_tool_regex`
109
+ * instead -- so any model-facing teaching string about `discover_tools`
110
+ * contradicts the actual tool list and must be suppressed (260428-oyc).
111
+ *
112
+ * Lowercase-normalize so provider-prefixed model ids
113
+ * (`anthropic/claude-sonnet-4`, `bedrock/anthropic.claude-opus-4`) resolve
114
+ * correctly.
115
+ */
116
+ export declare function supportsToolSearch(modelId: string): boolean;
103
117
  /**
104
118
  * Extract recently-used tool names from session history messages.
105
119
  * Looks at the most recent N assistant messages for tool_use blocks.
@@ -119,12 +133,33 @@ export declare function resolveToolDescription(tool: ToolDefinition): string;
119
133
  /**
120
134
  * Build a `<deferred-tools>` XML block for dynamic preamble injection.
121
135
  * Lists deferred tool names and descriptions so the LLM knows what's
122
- * available behind discover_tools.
136
+ * available behind a discovery mechanism.
137
+ *
138
+ * The third line (the instruction line) is conditional on `useToolSearch`:
139
+ *
140
+ * - `useToolSearch=false` (default, every non-Anthropic provider + Haiku):
141
+ * teaches the model to call the client-side `discover_tools` tool, which
142
+ * IS present in those payloads.
143
+ *
144
+ * - `useToolSearch=true` (Anthropic Sonnet/Opus 4.x): the API payload no
145
+ * longer contains a client-side `discover_tools` tool -- the
146
+ * request-body-injector replaces it with the server-side
147
+ * `tool_search_tool_regex` and marks deferred tools `defer_loading: true`,
148
+ * meaning Anthropic auto-loads them on first direct invocation. The
149
+ * teaching string therefore points at direct invocation + tool-search by
150
+ * regex, never at `discover_tools`. Without this conditional, the model
151
+ * reads its own preamble ("call discover_tools") against a tool list that
152
+ * doesn't contain that tool and gives up (260428-oyc production repro).
123
153
  *
124
154
  * @param entries - Deferred tool entries (remaining after discovery re-inclusion)
155
+ * @param options - Optional flags. `useToolSearch=true` switches the third
156
+ * line to the tool-search-aware variant. Defaults to false (backward-
157
+ * compatible with the discover_tools teaching).
125
158
  * @returns XML block string, or empty string when no entries
126
159
  */
127
- export declare function buildDeferredToolsContext(entries: DeferredToolEntry[]): string;
160
+ export declare function buildDeferredToolsContext(entries: DeferredToolEntry[], options?: {
161
+ useToolSearch?: boolean;
162
+ }): string;
128
163
  /**
129
164
  * Apply unified tool deferral: rule-based, budget-based, small-model,
130
165
  * lifecycle merge, and operator overrides.
@@ -95,6 +95,25 @@ export function resolveModelTier(contextWindow) {
95
95
  export function resolveToolCallingTemperature(modelTier) {
96
96
  return modelTier === "small" ? 0.0 : 0.1;
97
97
  }
98
+ /**
99
+ * Anthropic models that support server-side tool_search_tool_regex
100
+ * (defer_loading). Sonnet 4.x+, Opus 4.x+; NOT Haiku.
101
+ *
102
+ * When this returns true, request-body-injector strips client-side
103
+ * `discover_tools` from the API payload and appends `tool_search_tool_regex`
104
+ * instead -- so any model-facing teaching string about `discover_tools`
105
+ * contradicts the actual tool list and must be suppressed (260428-oyc).
106
+ *
107
+ * Lowercase-normalize so provider-prefixed model ids
108
+ * (`anthropic/claude-sonnet-4`, `bedrock/anthropic.claude-opus-4`) resolve
109
+ * correctly.
110
+ */
111
+ export function supportsToolSearch(modelId) {
112
+ const lower = modelId.toLowerCase();
113
+ if (lower.includes("haiku"))
114
+ return false;
115
+ return lower.includes("sonnet") || lower.includes("opus");
116
+ }
98
117
  // ---------------------------------------------------------------------------
99
118
  // Recently-used tool extraction
100
119
  // ---------------------------------------------------------------------------
@@ -148,14 +167,34 @@ export function resolveToolDescription(tool) {
148
167
  /**
149
168
  * Build a `<deferred-tools>` XML block for dynamic preamble injection.
150
169
  * Lists deferred tool names and descriptions so the LLM knows what's
151
- * available behind discover_tools.
170
+ * available behind a discovery mechanism.
171
+ *
172
+ * The third line (the instruction line) is conditional on `useToolSearch`:
173
+ *
174
+ * - `useToolSearch=false` (default, every non-Anthropic provider + Haiku):
175
+ * teaches the model to call the client-side `discover_tools` tool, which
176
+ * IS present in those payloads.
177
+ *
178
+ * - `useToolSearch=true` (Anthropic Sonnet/Opus 4.x): the API payload no
179
+ * longer contains a client-side `discover_tools` tool -- the
180
+ * request-body-injector replaces it with the server-side
181
+ * `tool_search_tool_regex` and marks deferred tools `defer_loading: true`,
182
+ * meaning Anthropic auto-loads them on first direct invocation. The
183
+ * teaching string therefore points at direct invocation + tool-search by
184
+ * regex, never at `discover_tools`. Without this conditional, the model
185
+ * reads its own preamble ("call discover_tools") against a tool list that
186
+ * doesn't contain that tool and gives up (260428-oyc production repro).
152
187
  *
153
188
  * @param entries - Deferred tool entries (remaining after discovery re-inclusion)
189
+ * @param options - Optional flags. `useToolSearch=true` switches the third
190
+ * line to the tool-search-aware variant. Defaults to false (backward-
191
+ * compatible with the discover_tools teaching).
154
192
  * @returns XML block string, or empty string when no entries
155
193
  */
156
- export function buildDeferredToolsContext(entries) {
194
+ export function buildDeferredToolsContext(entries, options) {
157
195
  if (entries.length === 0)
158
196
  return "";
197
+ const useToolSearch = options?.useToolSearch === true;
159
198
  // Separate MCP tools (group by server) from non-MCP tools (individual listing)
160
199
  const mcpByServer = new Map();
161
200
  const nonMcpEntries = [];
@@ -181,10 +220,13 @@ export function buildDeferredToolsContext(entries) {
181
220
  const shortNames = tools.map(t => t.name.startsWith(prefix) ? t.name.slice(prefix.length) : t.name);
182
221
  lines.push(`[${server}] (${tools.length} tools): ${shortNames.join(", ")}`);
183
222
  }
223
+ const instruction = useToolSearch
224
+ ? "These tools auto-load on first invocation -- call them directly by name with the right arguments. To preview a tool's schema before calling, use tool_search_tool_regex with a regex matching the tool name (e.g., tool_search_tool_regex(pattern: \"agents_manage\"))."
225
+ : "Call discover_tools to search by keyword or server name (e.g., discover_tools(\"yfinance\")).";
184
226
  return [
185
227
  "<deferred-tools>",
186
228
  "The following tools are available but not loaded.",
187
- "Call discover_tools to search by keyword or server name (e.g., discover_tools(\"yfinance\")).",
229
+ instruction,
188
230
  "",
189
231
  ...lines,
190
232
  "</deferred-tools>",
@@ -41,7 +41,6 @@ export const READ_ONLY_TOOLS = new Set([
41
41
  "sessions_list",
42
42
  "session_status",
43
43
  "sessions_history",
44
- "agents_list",
45
44
  // Context reads
46
45
  "ctx_search",
47
46
  "ctx_inspect",
@@ -59,14 +59,23 @@ export interface ExecutionResult {
59
59
  /** Stop reason from tracker (budget_reached | diminishing_returns | max_continuations | under_budget). */
60
60
  stopReason: string;
61
61
  };
62
- /** Silent Execution Planner metrics (undefined if SEP inactive). */
62
+ /** Silent Execution Planner metrics (undefined if SEP inactive).
63
+ * SEP is observability-only post-L4: plan extraction + step counting
64
+ * remain; the legacy enforcement nudge was replaced by the post-batch
65
+ * continuation handler. */
63
66
  plannerMetrics?: {
64
67
  stepsPlanned: number;
65
68
  stepsCompleted: number;
66
69
  stepsSkipped: number;
67
- nudgeTriggered: boolean;
68
70
  planExtractionTurn: number;
69
71
  };
72
+ /** Post-batch continuation handler outcome (undefined when handler did
73
+ * not run, e.g., guardrail failed before reaching it). */
74
+ continuationMetrics?: {
75
+ fired: boolean;
76
+ attempts: number;
77
+ outcome: "recovered" | "still_empty" | "max_attempts_exhausted" | "disabled" | "no_match";
78
+ };
70
79
  }
71
80
  /** Optional overrides for per-execution behavior (e.g., sub-agent isolation). */
72
81
  export interface ExecutionOverrides {
@@ -52,6 +52,8 @@ export { createOAuthTokenManager } from "./model/oauth-token-manager.js";
52
52
  export type { OAuthTokenManager, OAuthTokenManagerDeps, OAuthError } from "./model/oauth-token-manager.js";
53
53
  export { createAuthUsageTracker } from "./model/auth-usage-tracker.js";
54
54
  export type { AuthUsageTracker, ProfileStats, ProfileUsageInput } from "./model/auth-usage-tracker.js";
55
+ export { createLastKnownModelTracker } from "./model/last-known-model.js";
56
+ export type { LastKnownModelTracker, LastKnownModelEntry } from "./model/last-known-model.js";
55
57
  export { createMessageRouter, resolveAgent } from "./routing/message-router.js";
56
58
  export type { MessageRouter } from "./routing/message-router.js";
57
59
  export { createSessionLifecycle } from "./session/session-lifecycle.js";
@@ -136,7 +138,7 @@ export type { PiEventBridgeDeps, PiEventBridgeResult } from "./bridge/pi-event-b
136
138
  export { createAuthStorageAdapter, DEFAULT_PROVIDER_KEYS } from "./model/auth-storage-adapter.js";
137
139
  export type { AuthStorageAdapterOptions } from "./model/auth-storage-adapter.js";
138
140
  export { createModelRegistryAdapter, registerCustomProviders, resolveInitialModel } from "./model/model-registry-adapter.js";
139
- export type { CustomProviderRegistration, CustomProviderLogger } from "./model/model-registry-adapter.js";
141
+ export type { CustomProviderRegistration, CustomProviderLogger, RegisterCustomProvidersResult } from "./model/model-registry-adapter.js";
140
142
  export { sessionKeyToPath, pathToSessionKey } from "./session/session-key-mapper.js";
141
143
  export { detectBrokenFollowThrough, FOLLOW_THROUGH_PATTERNS } from "./safety/response-safety-checks.js";
142
144
  export type { FollowThroughResult } from "./safety/response-safety-checks.js";
@@ -44,6 +44,8 @@ export { createModelScanner } from "./model/model-scanner.js";
44
44
  export { createOAuthTokenManager } from "./model/oauth-token-manager.js";
45
45
  // Auth usage tracker (from 62-05)
46
46
  export { createAuthUsageTracker } from "./model/auth-usage-tracker.js";
47
+ // Last-known-working model tracker (auth-failure fallback)
48
+ export { createLastKnownModelTracker } from "./model/last-known-model.js";
47
49
  // Routing
48
50
  export { createMessageRouter, resolveAgent } from "./routing/message-router.js";
49
51
  // Session lifecycle (renamed from session-manager.ts)
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Last-known-working model tracker.
3
+ *
4
+ * Tracks the most recent successfully-used model per agent across the daemon.
5
+ * When all configured fallbacks fail with auth errors (401/403), the retry
6
+ * pipeline can query this tracker for a model that recently worked -- either
7
+ * for the same agent or any other agent on the daemon.
8
+ *
9
+ * Follows the closure-over-mutable-state factory pattern (no classes),
10
+ * matching createProviderHealthMonitor.
11
+ *
12
+ * @module
13
+ */
14
+ /** A record of a model that successfully completed a prompt. */
15
+ export interface LastKnownModelEntry {
16
+ provider: string;
17
+ model: string;
18
+ timestamp: number;
19
+ }
20
+ /** Tracker interface for last-known-working model queries. */
21
+ export interface LastKnownModelTracker {
22
+ /** Record a successful model completion for an agent. */
23
+ recordSuccess(agentId: string, provider: string, model: string): void;
24
+ /** Get the last-known-working model for a specific agent. */
25
+ getLastKnown(agentId: string): LastKnownModelEntry | undefined;
26
+ /** Get any successful model from ANY agent (daemon-wide).
27
+ * Optionally exclude a specific provider (useful when that provider is failing). */
28
+ getAnyKnown(excludeProvider?: string): LastKnownModelEntry | undefined;
29
+ }
30
+ /**
31
+ * Create a last-known-working model tracker.
32
+ *
33
+ * Uses closure over mutable state (no classes) following the
34
+ * provider-health-monitor pattern. All operations are synchronous.
35
+ */
36
+ export declare function createLastKnownModelTracker(): LastKnownModelTracker;
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Last-known-working model tracker.
4
+ *
5
+ * Tracks the most recent successfully-used model per agent across the daemon.
6
+ * When all configured fallbacks fail with auth errors (401/403), the retry
7
+ * pipeline can query this tracker for a model that recently worked -- either
8
+ * for the same agent or any other agent on the daemon.
9
+ *
10
+ * Follows the closure-over-mutable-state factory pattern (no classes),
11
+ * matching createProviderHealthMonitor.
12
+ *
13
+ * @module
14
+ */
15
+ // ---------------------------------------------------------------------------
16
+ // Factory
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * Create a last-known-working model tracker.
20
+ *
21
+ * Uses closure over mutable state (no classes) following the
22
+ * provider-health-monitor pattern. All operations are synchronous.
23
+ */
24
+ export function createLastKnownModelTracker() {
25
+ const entries = new Map();
26
+ return {
27
+ recordSuccess(agentId, provider, model) {
28
+ entries.set(agentId, {
29
+ provider,
30
+ model,
31
+ timestamp: Date.now(),
32
+ });
33
+ },
34
+ getLastKnown(agentId) {
35
+ return entries.get(agentId);
36
+ },
37
+ getAnyKnown(excludeProvider) {
38
+ let best;
39
+ for (const entry of entries.values()) {
40
+ if (excludeProvider && entry.provider === excludeProvider)
41
+ continue;
42
+ if (!best || entry.timestamp > best.timestamp) {
43
+ best = entry;
44
+ }
45
+ }
46
+ return best;
47
+ },
48
+ };
49
+ }
@@ -58,6 +58,18 @@ export interface CustomProviderLogger {
58
58
  warn(obj: Record<string, unknown>, msg: string): void;
59
59
  debug(obj: Record<string, unknown>, msg: string): void;
60
60
  }
61
+ /** Result of custom provider registration. */
62
+ export interface RegisterCustomProvidersResult {
63
+ /** Number of provider entries successfully registered. */
64
+ registered: number;
65
+ /**
66
+ * Comis provider name → built-in pi SDK provider name.
67
+ * Populated when a YAML entry's `type` matches a built-in provider but the
68
+ * entry's key (comis name) differs. Lets model resolution fall back to the
69
+ * built-in catalog: `registry.find("gemini", id)` fails → try `registry.find("google", id)`.
70
+ */
71
+ providerAliases: Map<string, string>;
72
+ }
61
73
  /**
62
74
  * Register YAML `providers.entries.*` with pi-coding-agent's ModelRegistry.
63
75
  *
@@ -69,13 +81,13 @@ export interface CustomProviderLogger {
69
81
  * Per-entry behavior:
70
82
  * - Skipped if `enabled === false`.
71
83
  * - Skipped if no models declared and no `baseUrl` override.
84
+ * - Models that already exist in the built-in pi SDK catalog for the
85
+ * entry's `type` are filtered out (no redundant registration).
72
86
  * - On `registerProvider` error (missing baseUrl, missing apiKey, etc.),
73
87
  * a WARN is logged and the loop continues -- one bad entry must not
74
88
  * prevent the daemon from starting.
75
- *
76
- * @returns Number of entries successfully registered.
77
89
  */
78
- export declare function registerCustomProviders(registry: ModelRegistry, entries: Record<string, CustomProviderRegistration>, secretManager: SecretManager, logger: CustomProviderLogger): number;
90
+ export declare function registerCustomProviders(registry: ModelRegistry, entries: Record<string, CustomProviderRegistration>, secretManager: SecretManager, logger: CustomProviderLogger): RegisterCustomProvidersResult;
79
91
  /**
80
92
  * Resolve the initial model for an agent session.
81
93
  *
@@ -90,4 +102,4 @@ export declare function registerCustomProviders(registry: ModelRegistry, entries
90
102
  export declare function resolveInitialModel(registry: ModelRegistry, config: {
91
103
  provider: string;
92
104
  model: string;
93
- }, allowlist?: ModelAllowlist): Promise<InitialModelResult>;
105
+ }, allowlist?: ModelAllowlist, providerAliases?: Map<string, string>): Promise<InitialModelResult>;