@aragon-agent/cli 0.6.38 → 0.6.40
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/CHANGELOG.md +263 -0
- package/README.md +544 -11
- package/dist/agent/controller.d.ts +245 -10
- package/dist/agent/controller.js +367 -22
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +70 -2
- package/dist/agent/reducer.js +59 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +28 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +106 -32
- package/dist/commands/builtins.js +24 -3
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +55 -3
- package/dist/config/schema.d.ts +125 -10
- package/dist/config/schema.js +138 -14
- package/dist/config/store.js +23 -3
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +238 -12
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
package/dist/agent/controller.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resolveModelProfileState } from '../config/model-profile-resolution.js'
|
|
|
11
11
|
import { makeGetApiKey } from '../config/load.js';
|
|
12
12
|
import { updatePersistedConfig } from '../config/store.js';
|
|
13
13
|
import { ModelWindows } from '../config/model-windows.js';
|
|
14
|
-
import { createBuiltinTools } from '../tools/index.js';
|
|
14
|
+
import { createBuiltinTools, gatedToolTimeouts, } from '../tools/index.js';
|
|
15
15
|
import { createFileChangeStore } from '../tools/file-change-store.js';
|
|
16
16
|
import { createToolOutputStore, } from '../tools/tool-output-store.js';
|
|
17
17
|
import { TeamRuntime } from '../team/runtime.js';
|
|
@@ -34,18 +34,25 @@ import { getHomeRoot } from '../config/app-paths.js';
|
|
|
34
34
|
import { addSignalHook } from '../logging/install.js';
|
|
35
35
|
import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
|
|
36
36
|
import { FastWiring, offFastStatus } from '../fast/wiring.js';
|
|
37
|
+
// ONE-SHOT FAST CALLS (tui-shortcut-drawer-and-task-summary §6.1). The TYPES come
|
|
38
|
+
// from `fast/task-summary-types.ts` and only `fast/` produces them, so this module
|
|
39
|
+
// never learns how a label is made - it owns the controller and the epoch, which
|
|
40
|
+
// is the pair `runTaskSummaryCall` is forbidden to own (see its header).
|
|
41
|
+
import { createTaskSummaryDeps, runTaskSummaryCall } from '../fast/summary-call.js';
|
|
37
42
|
import { CompactionWiring, offCompactionSnapshot } from '../compaction/wiring.js';
|
|
38
43
|
import { ContextMeter } from '../compaction/meter.js';
|
|
39
44
|
import { TodoStore } from '../todo/store.js';
|
|
40
45
|
import { createTodoTool } from '../todo/todo-tool.js';
|
|
41
46
|
import { buildTodoBlock } from '../todo/prompt.js';
|
|
42
47
|
import { createPlanTools } from '../tools/plan-tools.js';
|
|
43
|
-
import { DENY_ALL_HUMAN_INPUT } from '../tools/human-input.js';
|
|
48
|
+
import { ASK_GUARD_LIMITS, DENY_ALL_HUMAN_INPUT, questionFingerprint, } from '../tools/human-input.js';
|
|
44
49
|
import { getLogger } from '../logging/logger.js';
|
|
45
50
|
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
46
51
|
import { SkillService } from '../skills/service.js';
|
|
47
52
|
import { createSkillTools } from '../skills/tools.js';
|
|
53
|
+
import { SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
|
|
48
54
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
55
|
+
import { buildSkillTriageBlock } from './skill-triage-prompt.js';
|
|
49
56
|
import { resetConversationWith, } from './conversation-reset.js';
|
|
50
57
|
import { archiveClearedConversation, } from '../session/clear-archive.js';
|
|
51
58
|
/**
|
|
@@ -74,9 +81,41 @@ export class AgentController {
|
|
|
74
81
|
modelMetadataAbort = new AbortController();
|
|
75
82
|
disposed = false;
|
|
76
83
|
tools;
|
|
84
|
+
/**
|
|
85
|
+
* Tools the CALLER serves, replaced on every `tool_register` (I-13).
|
|
86
|
+
*
|
|
87
|
+
* A SECOND ARRAY, NOT ONE GROWN ONE. `this.tools` is read by `listTools()`,
|
|
88
|
+
* `toolNames()`, `composeSystemPrompt()` and three "fixed at construction" flags
|
|
89
|
+
* (bash_output/bash_kill, task, todo_write). Growing it would silently change
|
|
90
|
+
* what those flags mean; keeping the caller's tools beside it gives one honest
|
|
91
|
+
* rule - the REGISTRY is the union, `this.tools` stays the host's own set.
|
|
92
|
+
*/
|
|
93
|
+
callerTools = [];
|
|
77
94
|
skills;
|
|
78
95
|
skillsEnabled;
|
|
79
96
|
onSkillsChanged;
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
// Skill triage (skill-triage-and-unbounded-ask §4.2.5) — PER-TURN state.
|
|
99
|
+
//
|
|
100
|
+
// Every field here is written in `prepareUserTurn` and cleared on `agent_end`,
|
|
101
|
+
// because a stale candidate set in the prompt is worse than no candidate set:
|
|
102
|
+
// it points the model at a skill chosen for a task it is no longer doing.
|
|
103
|
+
// -----------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* This turn's routing result, or `null` when triage is off or nothing scored.
|
|
106
|
+
*
|
|
107
|
+
* It also carries the task it was computed for (`SkillRoutingReport.task`), so
|
|
108
|
+
* there is no separate copy of the turn's text to keep in step with it.
|
|
109
|
+
*/
|
|
110
|
+
turnRouting = null;
|
|
111
|
+
/** The previous turn's routing, for `/skills route` with no argument. */
|
|
112
|
+
lastSkillRouting = null;
|
|
113
|
+
/** Did this turn call `skill(name)` successfully? The only proof of adoption. */
|
|
114
|
+
skillLoadedThisTurn = false;
|
|
115
|
+
/** Tool calls this turn, EXCLUDING `skill` / `skill_find` (see §4.2.7). */
|
|
116
|
+
toolCallsThisTurn = 0;
|
|
117
|
+
/** Skills already reported as "not loaded" — a session says it once (I-T5). */
|
|
118
|
+
skillTriageNoticed = new Set();
|
|
80
119
|
/** Mutable session cwd — tools resolve relative paths against this. */
|
|
81
120
|
cwd;
|
|
82
121
|
/**
|
|
@@ -88,6 +127,17 @@ export class AgentController {
|
|
|
88
127
|
* Empty on every path but `aragon exec`.
|
|
89
128
|
*/
|
|
90
129
|
appendSystemPrompt;
|
|
130
|
+
/**
|
|
131
|
+
* The `<structured_result>` block (headless-request-scope-and-typed-result
|
|
132
|
+
* section 2.3.8), or `''`.
|
|
133
|
+
*
|
|
134
|
+
* A FIELD FOR THE SAME REASON `appendSystemPrompt` IS ONE: `composeSystemPrompt()`
|
|
135
|
+
* runs again on every rebuild, so a value spliced once at construction would vanish
|
|
136
|
+
* at the first `/cwd` change, skills rescan or mode flip - and the model would then
|
|
137
|
+
* be asked to submit a result it had never been told about. Empty on every path but
|
|
138
|
+
* `aragon exec --result-schema`.
|
|
139
|
+
*/
|
|
140
|
+
structuredResultBlock;
|
|
91
141
|
// -----------------------------------------------------------------------
|
|
92
142
|
// Plan mode (§3.1) — THE CONTROLLER OWNS THE MODE.
|
|
93
143
|
//
|
|
@@ -98,7 +148,20 @@ export class AgentController {
|
|
|
98
148
|
// -----------------------------------------------------------------------
|
|
99
149
|
effectiveMode;
|
|
100
150
|
pendingMode = null;
|
|
101
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Fingerprints of the question SETS the user ANSWERED in this turn. Bounded,
|
|
153
|
+
* and the bound is storage hygiene rather than a refusal rule — see
|
|
154
|
+
* `ASK_GUARD_LIMITS`.
|
|
155
|
+
*
|
|
156
|
+
* Replaces a numeric round counter that capped `ask_user` at four calls. There
|
|
157
|
+
* is no cap now; the only thing this prevents is the same batch of questions
|
|
158
|
+
* being put to the user twice, which is noise rather than information.
|
|
159
|
+
*
|
|
160
|
+
* Cleared in `prepareUserTurn` for a NEW turn only. A steer continuation is the
|
|
161
|
+
* same turn — clearing there would disable the guard exactly when a long turn
|
|
162
|
+
* most needs it (§4.1.6).
|
|
163
|
+
*/
|
|
164
|
+
askedFingerprints = new Set();
|
|
102
165
|
/** Static: whether this session could ever render a question overlay. */
|
|
103
166
|
planInteractive;
|
|
104
167
|
// -----------------------------------------------------------------------
|
|
@@ -308,6 +371,10 @@ export class AgentController {
|
|
|
308
371
|
this.config = config;
|
|
309
372
|
this.cwd = config.cwd;
|
|
310
373
|
this.appendSystemPrompt = deps.appendSystemPrompt ?? '';
|
|
374
|
+
// ABOVE `createBuiltinTools` FOR THE REASON THE PROMPT FIELD IS: the tool has to
|
|
375
|
+
// exist before the array is built, and the block has to exist before the prompt is
|
|
376
|
+
// composed - and both are read again on every rebuild.
|
|
377
|
+
this.structuredResultBlock = deps.structuredResultBlock ?? '';
|
|
311
378
|
const humanInput = deps.humanInput ?? DENY_ALL_HUMAN_INPUT;
|
|
312
379
|
this.planInteractive = humanInput.neverPrompts !== true;
|
|
313
380
|
this.effectiveMode = config.startInPlanMode ? 'plan' : 'build';
|
|
@@ -431,6 +498,14 @@ export class AgentController {
|
|
|
431
498
|
// mid-dispatch and abort it at `idleTimeoutMs`. The queue pauses the
|
|
432
499
|
// CHILD's watchdog instead, which is the one that needs it (P0-4).
|
|
433
500
|
...(confirm ? { humanQueue: new TeamHumanQueue(confirm) } : {}),
|
|
501
|
+
// SITE ONE OF TWO FOR THE CHILD GATE (headless-host-bridge 2.3.7). The
|
|
502
|
+
// second is `runtime.ts::subDeps`; declaring it here and forgetting that
|
|
503
|
+
// one type-checks and forwards nothing, and every child would then run
|
|
504
|
+
// unapproved while the caller believes it is approving.
|
|
505
|
+
...(deps.toolGateForChild ? { toolGateFor: deps.toolGateForChild } : {}),
|
|
506
|
+
...(deps.decisionBudgetMs !== undefined
|
|
507
|
+
? { decisionBudgetMs: deps.decisionBudgetMs }
|
|
508
|
+
: {}),
|
|
434
509
|
// CHILDREN INHERIT THE POLICY (§4.2 invariant 3 / R-4 / AC-12).
|
|
435
510
|
// Without this line `--deny-tool bash` is bypassed by one `task` call
|
|
436
511
|
// and nothing anywhere reports it. Spread conditionally so a session
|
|
@@ -453,7 +528,12 @@ export class AgentController {
|
|
|
453
528
|
this.jobsRegistered = config.jobs.enabled && config.bash.background;
|
|
454
529
|
this.jobMirror =
|
|
455
530
|
this.jobsRegistered && config.jobs.persistLog
|
|
456
|
-
? new JobLogMirror({
|
|
531
|
+
? new JobLogMirror({
|
|
532
|
+
root: () => join(getHomeRoot(), 'jobs', this.jobSessionTag),
|
|
533
|
+
// NAMED, NOT DERIVED. The mirror sweeps only this directory; the session
|
|
534
|
+
// tagging above is what keeps its own run out of the sweep's reach.
|
|
535
|
+
jobsRoot: () => join(getHomeRoot(), 'jobs'),
|
|
536
|
+
})
|
|
457
537
|
: undefined;
|
|
458
538
|
this.procs = new ProcSupervisor({
|
|
459
539
|
readyTimeoutMs: () => this.config.bash.readyTimeoutMs,
|
|
@@ -547,7 +627,13 @@ export class AgentController {
|
|
|
547
627
|
// (or 11 with skills) it is today.
|
|
548
628
|
planTools: createPlanTools({
|
|
549
629
|
gate: humanInput,
|
|
550
|
-
|
|
630
|
+
// TWO CALLBACKS, AND THE SPLIT IS LOAD-BEARING (§4.1.2). One
|
|
631
|
+
// read-and-write accessor could only be called before the question
|
|
632
|
+
// (recording an answer nobody gave, which makes "a cancelled round stays
|
|
633
|
+
// askable" impossible) or after it (which cannot stop a repeat). The guard
|
|
634
|
+
// is "read before, write only after a real answer".
|
|
635
|
+
hasAnsweredQuestions: (questions) => this.hasAnswered(questions),
|
|
636
|
+
noteAnsweredQuestions: (questions) => this.noteAnswered(questions),
|
|
551
637
|
withHumanWait: (fn) => this.withHumanWait(fn),
|
|
552
638
|
onPlanApproved: () => this.setAgentMode('build', { force: true }),
|
|
553
639
|
}),
|
|
@@ -599,6 +685,15 @@ export class AgentController {
|
|
|
599
685
|
}),
|
|
600
686
|
]
|
|
601
687
|
: [],
|
|
688
|
+
// SPREAD, NEVER `resultTool: deps.resultTool` (the D-12 lesson, and the shape
|
|
689
|
+
// every optional dep in this bag follows). With no schema the options bag has no
|
|
690
|
+
// such key, so `createBuiltinTools` adds nothing and the tool array is the one it
|
|
691
|
+
// builds today - which is what makes I-S2 provable by object identity.
|
|
692
|
+
...(deps.resultTool ? { resultTool: deps.resultTool } : {}),
|
|
693
|
+
// THE HOOK PAIR, SPREAD FOR THE SAME REASON AND AT THE SAME SITE (D-12): a dep that
|
|
694
|
+
// is declared but not forwarded is dropped in silence, and the only symptom is that
|
|
695
|
+
// the caller's hooks never fire.
|
|
696
|
+
...(deps.hookRunner ? { hookRunner: deps.hookRunner } : {}),
|
|
602
697
|
// The PULL port. Absent when jobs are off, which leaves every tool object
|
|
603
698
|
// untouched and provable by identity.
|
|
604
699
|
...(this.jobsRegistered
|
|
@@ -663,6 +758,21 @@ export class AgentController {
|
|
|
663
758
|
// today — which is what makes "the TUI and `-p` are unchanged" provable by
|
|
664
759
|
// identity rather than by inspection.
|
|
665
760
|
...(deps.permission ? { permission: deps.permission } : {}),
|
|
761
|
+
// THE CALLER'S APPROVAL GATE, spread the same way and for the same reason
|
|
762
|
+
// (headless-host-bridge section 2.3.3): with no gate the options bag has no
|
|
763
|
+
// such key, so `createBuiltinTools` never enters its mapping branch and the
|
|
764
|
+
// tool OBJECTS are the ones it builds today.
|
|
765
|
+
//
|
|
766
|
+
// THE PAUSE IS `withHumanWait`, NOT A SECOND WRAPPER. `withPausedWatchdog` is
|
|
767
|
+
// documented as not reentrant, and this is the same function the plan tools
|
|
768
|
+
// already receive - so an approval wait and a question wait can never nest
|
|
769
|
+
// two pauses and resume the watchdog one level early.
|
|
770
|
+
...(deps.toolGate
|
|
771
|
+
? {
|
|
772
|
+
toolGate: deps.toolGate,
|
|
773
|
+
withToolWait: (fn) => this.withHumanWait(fn),
|
|
774
|
+
}
|
|
775
|
+
: {}),
|
|
666
776
|
// Omitted entirely when skills are off, which is what leaves the seven
|
|
667
777
|
// built-ins unwrapped and AC-G17 provable by object identity.
|
|
668
778
|
...(this.skillsEnabled
|
|
@@ -747,6 +857,22 @@ export class AgentController {
|
|
|
747
857
|
toolTimeoutOverrides: {
|
|
748
858
|
ask_user: config.planModeHumanTimeoutMs,
|
|
749
859
|
submit_plan: config.planModeHumanTimeoutMs,
|
|
860
|
+
// THE TWO PHASES, ADDED (headless-host-bridge §2.3.4). The executor
|
|
861
|
+
// arms its timer BEFORE the approval wrapper runs, so a ceiling of
|
|
862
|
+
// `toolTimeoutMs` alone would leave the tool less than its configured
|
|
863
|
+
// budget by exactly the time a human took to answer. ADMITTED
|
|
864
|
+
// OVER-GRANT: a caller that answers instantly still gets the sum. The
|
|
865
|
+
// opposite trade - charging the wait to the tool - would make "the
|
|
866
|
+
// human thought for two minutes" read as "this command is too slow",
|
|
867
|
+
// and a tool timeout is supposed to mean the latter.
|
|
868
|
+
//
|
|
869
|
+
// THE KEY SET IS `CALLER_GATED_TOOLS`, the same constant
|
|
870
|
+
// `ToolGate.applies` is built from (I-6): two lists here would drift,
|
|
871
|
+
// and the drift would be a gated tool with no wait budget - a
|
|
872
|
+
// five-minute approval that kills its own tool at 180 s.
|
|
873
|
+
...(deps.decisionBudgetMs !== undefined
|
|
874
|
+
? gatedToolTimeouts(config.toolTimeoutMs, deps.decisionBudgetMs)
|
|
875
|
+
: {}),
|
|
750
876
|
// A fan-out is not a hung tool either. Like the two above, this
|
|
751
877
|
// number only bites BECAUSE `task` races `ctx.signal` — the
|
|
752
878
|
// executor's timeout is cooperative, so an override is inert for any
|
|
@@ -831,6 +957,17 @@ export class AgentController {
|
|
|
831
957
|
this.activeToolCalls += 1;
|
|
832
958
|
if (event.type === 'tool_execution_end') {
|
|
833
959
|
this.activeToolCalls = Math.max(0, this.activeToolCalls - 1);
|
|
960
|
+
// The ONLY proof a skill was adopted is a successful `skill` call. Reading
|
|
961
|
+
// the model's prose for a skill name would be a heuristic; this is a fact
|
|
962
|
+
// the executor already reports.
|
|
963
|
+
if (event.toolName === 'skill' && event.isError !== true)
|
|
964
|
+
this.skillLoadedThisTurn = true;
|
|
965
|
+
// `skill` / `skill_find` do not count as "work done": the notice below is
|
|
966
|
+
// for a turn that changed something without reading its skill, and a turn
|
|
967
|
+
// that ONLY searched for a skill is a turn that already tried.
|
|
968
|
+
if (event.toolName !== 'skill' && event.toolName !== 'skill_find') {
|
|
969
|
+
this.toolCallsThisTurn += 1;
|
|
970
|
+
}
|
|
834
971
|
// THE ONE PUSH SITE. A notice that could not be pushed here is picked up
|
|
835
972
|
// by the next tool result (the pull channel) or by the next user turn
|
|
836
973
|
// (the fill channel), so nothing is lost by declining.
|
|
@@ -847,6 +984,9 @@ export class AgentController {
|
|
|
847
984
|
// required: `turn_start` covers the next turn of the same run, and this
|
|
848
985
|
// covers a run that ended with a notice stranded in the queue.
|
|
849
986
|
this.jobRelay?.requeueUndelivered();
|
|
987
|
+
// AFTER the requeue, so a turn that ends with a stranded notice still gets
|
|
988
|
+
// its feedback line. Order between the two is otherwise irrelevant.
|
|
989
|
+
this.maybeReportSkillTriage();
|
|
850
990
|
}
|
|
851
991
|
});
|
|
852
992
|
if (this.fast) {
|
|
@@ -895,13 +1035,27 @@ export class AgentController {
|
|
|
895
1035
|
const tier = resolveFastTier(config, (id, role) => !!makeGetApiKey(config, role)(id));
|
|
896
1036
|
const fastAvailable = config === this.config ? this.fast?.available() === true
|
|
897
1037
|
: this.fast?.isRegistered() && config.fast.enabled && tier.ok;
|
|
1038
|
+
const skillsCatalog = this.skillsEnabled ? this.skills.catalogBlock() : '';
|
|
1039
|
+
// READ LIVE FROM THE SERVICE, never `config.skills` (§4.2.5 / R-12).
|
|
1040
|
+
// `SkillService.updateConfig` replaces the SERVICE's own config object, so
|
|
1041
|
+
// `this.config.skills` goes stale the moment `/skills triage off` runs — and a
|
|
1042
|
+
// stale read would leave the block in the prompt until the next launch, which
|
|
1043
|
+
// looks exactly like a broken switch. Same discipline as `/todo off`.
|
|
1044
|
+
const triageOn = this.skillsEnabled && this.skills.getConfig().triage === true;
|
|
1045
|
+
// An empty catalog means there is nothing to triage against, so the block is
|
|
1046
|
+
// absent rather than present-and-useless (I-T1 / AC-T18).
|
|
1047
|
+
const triageBlock = triageOn && skillsCatalog.length > 0
|
|
1048
|
+
? buildSkillTriageBlock({ candidates: this.turnRouting?.candidates ?? [] })
|
|
1049
|
+
: '';
|
|
898
1050
|
return buildSystemPrompt({
|
|
899
1051
|
cwd: this.cwd,
|
|
900
1052
|
tools: this.tools,
|
|
901
|
-
skillsBlock: this.skillsEnabled ?
|
|
1053
|
+
skillsBlock: this.skillsEnabled ? skillsCatalog + this.skills.alwaysBlock() : '',
|
|
1054
|
+
// Immediately after the catalog it refers to, before the plan block whose
|
|
1055
|
+
// first step defers to it (§4.2.4).
|
|
1056
|
+
skillTriageBlock: triageBlock,
|
|
902
1057
|
agentMode: this.effectiveMode,
|
|
903
1058
|
planInteractive: this.planInteractive,
|
|
904
|
-
planMaxAskRounds: config.planModeMaxAskRounds,
|
|
905
1059
|
// BOTH flags, and both are required (§4.5). `teamRegistered` false means
|
|
906
1060
|
// there is no `task` tool to advertise; `teamEnabled` false means the user
|
|
907
1061
|
// turned it off for this session. Advertising in either case would
|
|
@@ -946,6 +1100,11 @@ export class AgentController {
|
|
|
946
1100
|
// tools that make its promises achievable are registered. With the flag off
|
|
947
1101
|
// the prompt is byte-identical to the pre-feature text (I-3).
|
|
948
1102
|
jobsBlock: this.jobsRegistered ? buildBackgroundJobsBlock() : '',
|
|
1103
|
+
// FROM THE FIELD, so the block survives every rebuild for the reason the field's
|
|
1104
|
+
// own comment records. Empty everywhere but `aragon exec --result-schema`, and
|
|
1105
|
+
// `buildSystemPrompt` splices it conditionally - so every existing prompt is
|
|
1106
|
+
// byte-identical (I-S1).
|
|
1107
|
+
structuredResultBlock: this.structuredResultBlock,
|
|
949
1108
|
// Read from the FIELD on every rebuild, for the reason the field's own
|
|
950
1109
|
// comment gives. Empty everywhere but `aragon exec`, and `buildSystemPrompt`
|
|
951
1110
|
// splices it conditionally, so every existing prompt is byte-identical.
|
|
@@ -1100,7 +1259,7 @@ export class AgentController {
|
|
|
1100
1259
|
}
|
|
1101
1260
|
/** Prepare before clearing the plan, then enter the engine without yielding. */
|
|
1102
1261
|
async startPrompt(text, options, request) {
|
|
1103
|
-
this.prepareUserTurn(text);
|
|
1262
|
+
this.prepareUserTurn(text, { steering: false });
|
|
1104
1263
|
if (request !== this.startupSequence) {
|
|
1105
1264
|
return { status: 'not-started', reason: 'cancelled' };
|
|
1106
1265
|
}
|
|
@@ -1129,10 +1288,39 @@ export class AgentController {
|
|
|
1129
1288
|
// failure AC-25 exists to prevent.
|
|
1130
1289
|
this.jobRelay?.flushForTurn();
|
|
1131
1290
|
}
|
|
1132
|
-
|
|
1291
|
+
/**
|
|
1292
|
+
* THE BOUNDARY OF ONE TURN — and there are TWO call sites, which is the reason
|
|
1293
|
+
* this takes an explicit flag instead of resetting everything unconditionally.
|
|
1294
|
+
*
|
|
1295
|
+
* `startPrompt` passes `{ steering: false }`: the user typed something new, so
|
|
1296
|
+
* this really is a new turn. `continue()` passes `{ steering: true }` with the
|
|
1297
|
+
* CONCATENATED queued steering text, which resumes the SAME turn.
|
|
1298
|
+
*
|
|
1299
|
+
* A steer continuation must not re-triage (the candidates would change mid-turn,
|
|
1300
|
+
* and rebuilding the prompt aborts an idle compaction for no gain) and must not
|
|
1301
|
+
* clear the answered-question guard (a long turn interrupted by one steer is
|
|
1302
|
+
* exactly when a repeat question is most likely). Hence per-turn state is
|
|
1303
|
+
* initialised in ONE branch.
|
|
1304
|
+
*
|
|
1305
|
+
* `steering` is REQUIRED and positional-second so each call site states which
|
|
1306
|
+
* kind of turn it is starting, rather than letting a default make "new turn" the
|
|
1307
|
+
* thoughtless option.
|
|
1308
|
+
*/
|
|
1309
|
+
prepareUserTurn(text, options) {
|
|
1133
1310
|
if (this.skillsEnabled)
|
|
1134
1311
|
this.skills.beginUserTurn();
|
|
1135
|
-
|
|
1312
|
+
if (!options.steering) {
|
|
1313
|
+
this.askedFingerprints.clear();
|
|
1314
|
+
this.turnRouting = this.skillsEnabled ? this.skills.triage(text) : null;
|
|
1315
|
+
this.skillLoadedThisTurn = false;
|
|
1316
|
+
this.toolCallsThisTurn = 0;
|
|
1317
|
+
// BEFORE the engine is entered, because `runAgentLoop` captures the system
|
|
1318
|
+
// prompt BY VALUE on its way in — a rebuild after `agent.prompt()` would be
|
|
1319
|
+
// one turn late. This is also why steer does not rebuild: `continue()` runs
|
|
1320
|
+
// while the loop already holds its snapshot, so the cost would be an idle
|
|
1321
|
+
// compaction aborted for a prompt nothing has read yet (§4.2.5).
|
|
1322
|
+
this.rebuildSystemPrompt();
|
|
1323
|
+
}
|
|
1136
1324
|
this.fast?.setGoal(text);
|
|
1137
1325
|
}
|
|
1138
1326
|
/** Cancel pending startup as well as any engine run already in progress. */
|
|
@@ -1310,7 +1498,7 @@ export class AgentController {
|
|
|
1310
1498
|
}
|
|
1311
1499
|
if (this.pendingUserSteering.size > 0) {
|
|
1312
1500
|
const request = ++this.startupSequence;
|
|
1313
|
-
this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'));
|
|
1501
|
+
this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'), { steering: true });
|
|
1314
1502
|
if (request !== this.startupSequence) {
|
|
1315
1503
|
return { status: 'not-started', reason: 'cancelled' };
|
|
1316
1504
|
}
|
|
@@ -1411,18 +1599,90 @@ export class AgentController {
|
|
|
1411
1599
|
return {
|
|
1412
1600
|
effective: this.effectiveMode,
|
|
1413
1601
|
pending: this.pendingMode,
|
|
1414
|
-
askRoundsUsed: this.askRounds,
|
|
1415
|
-
maxAskRounds: this.config.planModeMaxAskRounds,
|
|
1416
1602
|
};
|
|
1417
1603
|
}
|
|
1418
|
-
/**
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1604
|
+
/**
|
|
1605
|
+
* READ-ONLY guard probe, called by `ask_user` BEFORE it opens the overlay.
|
|
1606
|
+
*
|
|
1607
|
+
* No side effect of any kind: a check that also writes cannot be placed before
|
|
1608
|
+
* the question without recording an answer that has not happened yet, and the
|
|
1609
|
+
* "a cancelled round stays askable" rule would then be unimplementable.
|
|
1610
|
+
*/
|
|
1611
|
+
hasAnswered(questions) {
|
|
1612
|
+
return this.askedFingerprints.has(questionFingerprint(questions));
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Record an ANSWERED question set, called by `ask_user` after a real answer.
|
|
1616
|
+
*
|
|
1617
|
+
* Evicts the oldest entry past `ASK_GUARD_LIMITS.maxRemembered`. That eviction
|
|
1618
|
+
* is memory hygiene and NOT a refusal rule — a bounded rejection would be the
|
|
1619
|
+
* deleted round cap wearing a bigger number.
|
|
1620
|
+
*/
|
|
1621
|
+
noteAnswered(questions) {
|
|
1622
|
+
this.askedFingerprints.add(questionFingerprint(questions));
|
|
1623
|
+
while (this.askedFingerprints.size > ASK_GUARD_LIMITS.maxRemembered) {
|
|
1624
|
+
const oldest = this.askedFingerprints.values().next();
|
|
1625
|
+
if (oldest.done === true)
|
|
1626
|
+
break;
|
|
1627
|
+
this.askedFingerprints.delete(oldest.value);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/** `/skills route` with no argument: the LAST turn's real routing result. */
|
|
1631
|
+
getSkillRouting() {
|
|
1632
|
+
return this.lastSkillRouting;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* One line when a confidently-matched skill was NOT loaded — and silence every
|
|
1636
|
+
* other time (D-8).
|
|
1637
|
+
*
|
|
1638
|
+
* THE DISCIPLINE IS "SILENT ON SUCCESS, VISIBLE ON FAILURE". Announcing the
|
|
1639
|
+
* candidates every turn would be ignored by the third day; announcing nothing
|
|
1640
|
+
* ever would make the whole feature unfalsifiable. "A skill covered this task,
|
|
1641
|
+
* you did the work anyway, and you never opened it" is precisely the failure this
|
|
1642
|
+
* feature exists to fix, so it is the one case that speaks — and it names the
|
|
1643
|
+
* command that explains it.
|
|
1644
|
+
*
|
|
1645
|
+
* SEVEN CONDITIONS, and the first one is the one that is easy to get wrong.
|
|
1646
|
+
*
|
|
1647
|
+
* 0. This turn was NOT aborted and NOT ended by an error.
|
|
1648
|
+
* `agent_end` is emitted unconditionally from a `finally` block in
|
|
1649
|
+
* `core/src/engine/agent.ts` and its payload carries no reason, no `aborted`
|
|
1650
|
+
* and no `error` field — so "did this turn finish normally?" has to be read
|
|
1651
|
+
* from this class's own state. `stopEngine()` sets `abortRequested` before it
|
|
1652
|
+
* calls `agent.abort()` and `agent_start` clears it, which makes it an exact
|
|
1653
|
+
* answer. Without this condition an Esc mis-touch would produce a line
|
|
1654
|
+
* criticising the model for not loading a skill: commentary in response to an
|
|
1655
|
+
* interruption.
|
|
1656
|
+
*
|
|
1657
|
+
* Conditions 5 and 0 are not redundant: 0 decides what counts as the end of a
|
|
1658
|
+
* turn, 5 decides whether the turn did any work. With only 5, a tool call made
|
|
1659
|
+
* before an abort would still trigger the notice.
|
|
1660
|
+
*/
|
|
1661
|
+
maybeReportSkillTriage() {
|
|
1662
|
+
const routing = this.turnRouting;
|
|
1663
|
+
const best = routing?.candidates[0];
|
|
1664
|
+
const shouldReport = this.abortRequested === false &&
|
|
1665
|
+
this.skillsEnabled &&
|
|
1666
|
+
this.skills.getConfig().triage === true &&
|
|
1667
|
+
routing !== null &&
|
|
1668
|
+
best !== undefined &&
|
|
1669
|
+
best.score >= SKILL_TRIAGE_LIMITS.confidentScore &&
|
|
1670
|
+
this.skillLoadedThisTurn === false &&
|
|
1671
|
+
this.toolCallsThisTurn >= 1 &&
|
|
1672
|
+
!this.skillTriageNoticed.has(best.name);
|
|
1673
|
+
if (shouldReport && best !== undefined) {
|
|
1674
|
+
this.skillTriageNoticed.add(best.name);
|
|
1675
|
+
this.notifyHostFn?.('info', `No skill was loaded; the closest installed match was "${best.name}". ` +
|
|
1676
|
+
'Run /skills route to see why.');
|
|
1423
1677
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1678
|
+
// The routing result outlives the turn so `/skills route` can explain it; the
|
|
1679
|
+
// candidates themselves must not, or the next `rebuildSystemPrompt()` would
|
|
1680
|
+
// resurrect a set computed for a task that is over.
|
|
1681
|
+
if (routing !== null)
|
|
1682
|
+
this.lastSkillRouting = routing;
|
|
1683
|
+
this.turnRouting = null;
|
|
1684
|
+
this.skillLoadedThisTurn = false;
|
|
1685
|
+
this.toolCallsThisTurn = 0;
|
|
1426
1686
|
}
|
|
1427
1687
|
/**
|
|
1428
1688
|
* Run `fn` with the idle watchdog paused.
|
|
@@ -2273,6 +2533,60 @@ export class AgentController {
|
|
|
2273
2533
|
fastSnapshot() {
|
|
2274
2534
|
return this.fast?.snapshot() ?? null;
|
|
2275
2535
|
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Summarise one user prompt into a header label, using the FAST tier only.
|
|
2538
|
+
*
|
|
2539
|
+
* NEVER REJECTS - the same contract `prompt()` has, and the reason `App` needs
|
|
2540
|
+
* no `.catch`. Every failure is folded into a discriminated
|
|
2541
|
+
* `TaskSummaryOutcome`, because a header label must not be able to produce an
|
|
2542
|
+
* unhandled rejection in a TUI that has nowhere to show one.
|
|
2543
|
+
*
|
|
2544
|
+
* THE CONTROLLER OWNS THE `AbortController` AND ABORTS IT UNCONDITIONALLY on the
|
|
2545
|
+
* way out: `runTaskSummaryCall` may return a `timeout` outcome while the request
|
|
2546
|
+
* is still open (it is forbidden to abort its own call - see that file's
|
|
2547
|
+
* header), so this `finally` is what actually releases the connection.
|
|
2548
|
+
*
|
|
2549
|
+
* EVERY `this.fast` READ IS BEHIND THE CLOSURE, exactly as the reviewer and the
|
|
2550
|
+
* overseer wiring do it: `complete` is only reached from inside the call, which
|
|
2551
|
+
* runs long after construction, so there is no use-before-assign.
|
|
2552
|
+
*/
|
|
2553
|
+
async summarizeTask(anchor, opts = {}) {
|
|
2554
|
+
const deps = createTaskSummaryDeps({
|
|
2555
|
+
getConfig: () => this.config,
|
|
2556
|
+
hasKey: (id, role) => this.hasApiKey(id, role),
|
|
2557
|
+
getApiKey: (id, role) => this.resolveKey(id, role),
|
|
2558
|
+
complete: (providerId, request) => {
|
|
2559
|
+
if (this.fast === null) {
|
|
2560
|
+
// A session whose tier never registered has no fast transport at all;
|
|
2561
|
+
// rejecting here is caught by `runTaskSummaryCall` and reported as a
|
|
2562
|
+
// tier reason rather than as a crash.
|
|
2563
|
+
return Promise.reject(new Error('Fast tier is not registered for this session.'));
|
|
2564
|
+
}
|
|
2565
|
+
return this.fast.completeViaFastRegistry(providerId, request);
|
|
2566
|
+
},
|
|
2567
|
+
...(opts.ellipsis ? { ellipsis: () => opts.ellipsis } : {}),
|
|
2568
|
+
});
|
|
2569
|
+
const controller = new AbortController();
|
|
2570
|
+
const upstream = opts.signal;
|
|
2571
|
+
const onAbort = () => controller.abort();
|
|
2572
|
+
if (upstream) {
|
|
2573
|
+
if (upstream.aborted)
|
|
2574
|
+
controller.abort();
|
|
2575
|
+
else
|
|
2576
|
+
upstream.addEventListener('abort', onAbort);
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
return await runTaskSummaryCall(deps, {
|
|
2580
|
+
anchor,
|
|
2581
|
+
signal: controller.signal,
|
|
2582
|
+
now: Date.now(),
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
finally {
|
|
2586
|
+
upstream?.removeEventListener('abort', onAbort);
|
|
2587
|
+
controller.abort();
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2276
2590
|
// -----------------------------------------------------------------------
|
|
2277
2591
|
// Context occupancy (context-usage-gauge-accuracy §4.2) — THREE FORWARDERS
|
|
2278
2592
|
//
|
|
@@ -2485,8 +2799,39 @@ export class AgentController {
|
|
|
2485
2799
|
return () => { };
|
|
2486
2800
|
return this.compaction.subscribe(listener);
|
|
2487
2801
|
}
|
|
2488
|
-
/**
|
|
2802
|
+
/**
|
|
2803
|
+
* List usable tools (name + description) for the /tools command.
|
|
2804
|
+
*
|
|
2805
|
+
* THE UNION, SO THE VIEW AND THE REGISTRY CANNOT DISAGREE (I-13). `system/init`
|
|
2806
|
+
* builds `init.tools` from here, as do `/tools` and `/skills`; reporting only the
|
|
2807
|
+
* host's own set while the model is calling `db_query` is the exact failure that
|
|
2808
|
+
* invariant names - a caller reading "the model has no such tool" about a tool it
|
|
2809
|
+
* is watching run.
|
|
2810
|
+
*/
|
|
2489
2811
|
listTools() {
|
|
2490
|
-
return this.tools;
|
|
2812
|
+
return this.callerTools.length === 0 ? this.tools : [...this.tools, ...this.callerTools];
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Replace the caller-served tool set. Called ONLY at a turn boundary.
|
|
2816
|
+
*
|
|
2817
|
+
* `Agent.setTools()` unregisters and re-registers entries in the LIVE registry.
|
|
2818
|
+
* This class already learned the lesson once: `submit_plan` mutates the mode from
|
|
2819
|
+
* INSIDE a tool execution, so rebuilding the array at that moment would mutate
|
|
2820
|
+
* the registry mid-iteration (see the comment on the plan tools). Hence the
|
|
2821
|
+
* refusal rather than a silently deferred write: a caller that registers
|
|
2822
|
+
* mid-turn gets an error it can act on, and the bridge defers the frame instead.
|
|
2823
|
+
*
|
|
2824
|
+
* `[]` is meaningful and not an omission: it means "restore the factory set".
|
|
2825
|
+
*/
|
|
2826
|
+
applyCallerTools(tools) {
|
|
2827
|
+
if (this.agent.state.isRunning) {
|
|
2828
|
+
throw new Error('applyCallerTools() called mid-turn; defer to the turn boundary.');
|
|
2829
|
+
}
|
|
2830
|
+
this.callerTools = [...tools];
|
|
2831
|
+
this.agent.setTools([...this.tools, ...this.callerTools]);
|
|
2832
|
+
}
|
|
2833
|
+
/** The names the caller currently serves, for `ack.tools`. */
|
|
2834
|
+
callerToolNames() {
|
|
2835
|
+
return this.callerTools.map((tool) => tool.name);
|
|
2491
2836
|
}
|
|
2492
2837
|
}
|
|
@@ -97,8 +97,19 @@ export function releasedSlots(draft) {
|
|
|
97
97
|
|| hasConnectionPatch(draft.patch, role))
|
|
98
98
|
released.add(role);
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
// ONLY A MODEL BINDING DECISION RELEASES THE SLOT (subagent-fanout D-7). The
|
|
101
|
+
// fan-out width and the supervisor live in the SAME `team` section but are not
|
|
102
|
+
// the kit's business: `{ maxSubagents: 6 }` used to hit the old
|
|
103
|
+
// `patch.team !== undefined` test, so narrowing the fan-out from the settings
|
|
104
|
+
// screen silently dropped the subagent model a session kit had installed for
|
|
105
|
+
// this session - with every value on screen still correct.
|
|
106
|
+
//
|
|
107
|
+
// `'model' in patch`, not `patch.model !== undefined`: an explicit
|
|
108
|
+
// `model: undefined` still means "this transaction spoke about the model".
|
|
109
|
+
const teamPatch = draft.patch.team;
|
|
110
|
+
if (teamPatch && ('model' in teamPatch || 'modelProfileId' in teamPatch)) {
|
|
101
111
|
released.add('subagent');
|
|
112
|
+
}
|
|
102
113
|
return [...released].filter((slot) => held.includes(slot));
|
|
103
114
|
}
|
|
104
115
|
/** The session kit as it stands AFTER this transaction's explicit decisions. */
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
* ASCII ONLY — `agent/` is inside the glyph scanner's scope.
|
|
9
9
|
*/
|
|
10
10
|
/** Bump when the wording changes, so a transcript can be dated. */
|
|
11
|
-
export declare const PLAN_MODE_BLOCK_VERSION = "
|
|
11
|
+
export declare const PLAN_MODE_BLOCK_VERSION = "v3-2026-10-26";
|
|
12
12
|
export interface PlanModeBlockOptions {
|
|
13
13
|
/** False under `-p` / piped stdin: no overlay can be rendered, so no tools. */
|
|
14
14
|
interactive: boolean;
|
|
15
|
-
/** `planModeMaxAskRounds`; substituted into the guidance so the two agree. */
|
|
16
|
-
maxAskRounds: number;
|
|
17
15
|
}
|
|
18
16
|
export declare function buildPlanModeBlock(options: PlanModeBlockOptions): string;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ASCII ONLY — `agent/` is inside the glyph scanner's scope.
|
|
9
9
|
*/
|
|
10
10
|
/** Bump when the wording changes, so a transcript can be dated. */
|
|
11
|
-
export const PLAN_MODE_BLOCK_VERSION = '
|
|
11
|
+
export const PLAN_MODE_BLOCK_VERSION = 'v3-2026-10-26';
|
|
12
12
|
/**
|
|
13
13
|
* The closing paragraph of the interactive block.
|
|
14
14
|
*
|
|
@@ -42,13 +42,18 @@ const HEADLESS_PARAGRAPH = [
|
|
|
42
42
|
' write the finished plan as your final message in markdown.',
|
|
43
43
|
].join('\n');
|
|
44
44
|
export function buildPlanModeBlock(options) {
|
|
45
|
-
const { interactive
|
|
45
|
+
const { interactive } = options;
|
|
46
|
+
// NO ROUND COUNT APPEARS HERE, and none may be reintroduced: the number used
|
|
47
|
+
// to be substituted from `planModeMaxAskRounds`, which meant the prompt itself
|
|
48
|
+
// told the model it would run out. "3-5 questions" was also wrong against a
|
|
49
|
+
// schema and normalizer that both accept 1-5; correcting it here is one line.
|
|
46
50
|
const askStep = interactive
|
|
47
51
|
? [
|
|
48
52
|
'2. If a decision would change the work and you cannot settle it by reading, call',
|
|
49
|
-
' ask_user with
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
' ask_user with 1-5 multiple-choice questions in ONE call. Mark exactly one option',
|
|
54
|
+
' recommended:true and put the reason in its description. There is NO round limit -',
|
|
55
|
+
' ask as many rounds as the work needs - but each round costs the user time, so batch',
|
|
56
|
+
' the questions and skip anything the workspace already answers.',
|
|
52
57
|
]
|
|
53
58
|
: [
|
|
54
59
|
'2. If a decision would change the work and you cannot settle it by reading, state',
|
|
@@ -76,8 +81,9 @@ export function buildPlanModeBlock(options) {
|
|
|
76
81
|
'',
|
|
77
82
|
'Work in this order:',
|
|
78
83
|
'1. Follow the project-guidance protocol: read applicable guidance and index navigation',
|
|
79
|
-
' before any source search or read. Then
|
|
80
|
-
'
|
|
84
|
+
' before any source search or read. Then run the skill triage described above and load',
|
|
85
|
+
' any skill that covers this task, BEFORE inspecting the real code with read_file,',
|
|
86
|
+
' list_dir, glob and grep. Never plan against unread structure.',
|
|
81
87
|
...askStep,
|
|
82
88
|
...submitStep,
|
|
83
89
|
'',
|