@aragon-agent/cli 0.6.45 → 0.6.47
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 +15 -0
- package/README.md +104 -6
- package/dist/agent/coalesce.d.ts +4 -0
- package/dist/agent/coalesce.js +13 -0
- package/dist/agent/controller.d.ts +21 -0
- package/dist/agent/controller.js +56 -3
- package/dist/agent/invocation-card.d.ts +77 -0
- package/dist/agent/invocation-card.js +169 -0
- package/dist/agent/prompt-options.d.ts +6 -0
- package/dist/agent/queued-messages.d.ts +3 -0
- package/dist/agent/queued-messages.js +4 -1
- package/dist/agent/reducer.d.ts +25 -0
- package/dist/agent/reducer.js +43 -5
- package/dist/agent/user-attention.d.ts +17 -0
- package/dist/agent/user-attention.js +32 -0
- package/dist/cli.js +2 -3
- package/dist/commands/builtins.js +12 -0
- package/dist/commands/governance-command.js +13 -1
- package/dist/commands/index-build.d.ts +8 -0
- package/dist/commands/index-build.js +11 -0
- package/dist/commands/registry.d.ts +23 -0
- package/dist/commands/registry.js +20 -7
- package/dist/commands/skills.js +36 -2
- package/dist/commands/subagent-command.js +3 -1
- package/dist/commands/team-command.d.ts +6 -0
- package/dist/commands/team-command.js +40 -3
- package/dist/config/load.js +4 -25
- package/dist/config/schema.d.ts +4 -4
- package/dist/config/schema.js +7 -1
- package/dist/proc/limits.d.ts +27 -13
- package/dist/proc/limits.js +27 -13
- package/dist/proc/progress.d.ts +7 -0
- package/dist/proc/progress.js +35 -7
- package/dist/proc/prompt.js +7 -4
- package/dist/proc/supervisor.js +7 -1
- package/dist/proc/types.d.ts +6 -0
- package/dist/proc/wait-plan.d.ts +39 -0
- package/dist/proc/wait-plan.js +77 -0
- package/dist/proc/wait-view.d.ts +65 -0
- package/dist/proc/wait-view.js +66 -0
- package/dist/proc/wait.d.ts +36 -3
- package/dist/proc/wait.js +67 -4
- package/dist/session/checkpoint.js +2 -0
- package/dist/session/persist.js +29 -2
- package/dist/session/validate-session.js +7 -2
- package/dist/team/forced-mode.d.ts +19 -0
- package/dist/team/forced-mode.js +42 -20
- package/dist/tools/job-tools.d.ts +13 -2
- package/dist/tools/job-tools.js +156 -33
- package/dist/ui/ActivityLine.js +5 -2
- package/dist/ui/App.js +59 -6
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/Transcript.js +11 -1
- package/dist/ui/entries/InvocationCard.d.ts +20 -0
- package/dist/ui/entries/InvocationCard.js +32 -0
- package/dist/ui/entries/ToolCard.js +11 -2
- package/dist/ui/entries/WaitCard.d.ts +37 -0
- package/dist/ui/entries/WaitCard.js +92 -0
- package/dist/ui/interaction-copy.d.ts +2 -0
- package/dist/ui/interaction-copy.js +3 -0
- package/dist/ui/layout/invocation-card.d.ts +41 -0
- package/dist/ui/layout/invocation-card.js +91 -0
- package/dist/ui/layout/status-layout.d.ts +1 -0
- package/dist/ui/layout/status-layout.js +10 -1
- package/dist/ui/layout/virtual-window.js +18 -2
- package/dist/ui/layout/wait-bar.d.ts +34 -0
- package/dist/ui/layout/wait-bar.js +48 -0
- package/dist/ui/run-status-row.d.ts +4 -2
- package/dist/ui/scrollback/live-preview.js +12 -0
- package/dist/ui/transcript-text.js +4 -1
- package/package.json +2 -2
package/dist/proc/wait.js
CHANGED
|
@@ -23,6 +23,20 @@
|
|
|
23
23
|
* nothing at all (the supervisor now emits `stopped` from it), and
|
|
24
24
|
* `dispose()` clears its listener set without emitting; the self-timeout is
|
|
25
25
|
* therefore a real second path, not belt-and-braces.
|
|
26
|
+
* 5. THE HEARTBEAT RE-CHECKS THE DEADLINE AGAINST `now()`, NOT AGAINST THE
|
|
27
|
+
* TIMER (extended-job-wait). A 20-minute wait spans a laptop lid: Node's
|
|
28
|
+
* monotonic `setTimeout` does not count the sleep, so a "10 minute" wait can
|
|
29
|
+
* run 40 minutes of wall clock. Each beat compares `now() - startedAt` with
|
|
30
|
+
* the budget and settles on the first beat after resume.
|
|
31
|
+
*
|
|
32
|
+
* THE WAKE SOURCE IS EDGE-TRIGGERED ONLY - there is no "is something pending"
|
|
33
|
+
* probe. The engine drains steering before EVERY tool execution
|
|
34
|
+
* (`core/src/engine/agent-loop.ts`), so input that arrived before this wait began
|
|
35
|
+
* has already been accepted; only input that arrives AFTER `subscribe()` needs to
|
|
36
|
+
* end the wait. Between the engine's decision to run `job_wait` and the
|
|
37
|
+
* `subscribe()` below there is no `await`, so no notification can fall into the
|
|
38
|
+
* gap. A level-triggered probe would add a stale-state hot loop (every wait
|
|
39
|
+
* returning at once) for no reachable gain.
|
|
26
40
|
*/
|
|
27
41
|
import { isTerminalStatus } from './types.js';
|
|
28
42
|
/** Read one bounded page per target; `read()` returns `undefined` for a vanished id. */
|
|
@@ -49,24 +63,41 @@ function allSettled(port, targets) {
|
|
|
49
63
|
return snapshot === undefined || isTerminalStatus(snapshot.status);
|
|
50
64
|
});
|
|
51
65
|
}
|
|
66
|
+
/** Has any target ended? Same unknown-id rule as `allSettled`. */
|
|
67
|
+
function anySettled(port, targets) {
|
|
68
|
+
return targets.some((id) => {
|
|
69
|
+
const snapshot = port.get(id);
|
|
70
|
+
return snapshot === undefined || isTerminalStatus(snapshot.status);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
52
73
|
/**
|
|
53
|
-
*
|
|
74
|
+
* THE ONE PREDICATE for "is this wait over", shared by the entry fast path and the
|
|
75
|
+
* `finish` funnel. Using it in only one of them makes `mode: 'any'` subscribe and
|
|
76
|
+
* burn the whole timeout when a target had already ended.
|
|
77
|
+
*/
|
|
78
|
+
function isDone(port, targets, mode) {
|
|
79
|
+
return mode === 'any' ? anySettled(port, targets) : allSettled(port, targets);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Wait up to `timeoutMs` for the targets to reach a terminal status.
|
|
54
83
|
*
|
|
55
84
|
* Resolves - never rejects - and always after at most `timeoutMs`. `now` is
|
|
56
85
|
* injectable so a test can assert `waitedMs` without depending on wall clock.
|
|
57
86
|
*/
|
|
58
87
|
export async function waitForJob(port, req, now = Date.now) {
|
|
59
88
|
const { targets, signal } = req;
|
|
89
|
+
const mode = req.mode ?? 'all';
|
|
60
90
|
const startedAt = now();
|
|
61
91
|
// RULE 1. Nothing to wait for at all, or everything already ended: no
|
|
62
92
|
// subscription, no timer, `waitedMs: 0`.
|
|
63
|
-
if (targets.length === 0 ||
|
|
93
|
+
if (targets.length === 0 || isDone(port, targets, mode)) {
|
|
64
94
|
return {
|
|
65
95
|
jobs: collect(port, targets),
|
|
66
96
|
terminal: true,
|
|
67
97
|
waitedMs: 0,
|
|
68
98
|
timedOut: false,
|
|
69
99
|
aborted: false,
|
|
100
|
+
woken: false,
|
|
70
101
|
pages: readPages(port, targets, req.since),
|
|
71
102
|
};
|
|
72
103
|
}
|
|
@@ -79,6 +110,8 @@ export async function waitForJob(port, req, now = Date.now) {
|
|
|
79
110
|
// from inside the listener instead of resolving the wait.
|
|
80
111
|
let unsubscribe = () => { };
|
|
81
112
|
let timer;
|
|
113
|
+
let beat;
|
|
114
|
+
let unwake = () => { };
|
|
82
115
|
/** Assigned below; declared here so `finish` never reads an uninitialised binding. */
|
|
83
116
|
let onAbort = () => { };
|
|
84
117
|
// RULE 3: the one funnel. Idempotent, and it releases every reference before
|
|
@@ -88,15 +121,19 @@ export async function waitForJob(port, req, now = Date.now) {
|
|
|
88
121
|
return;
|
|
89
122
|
done = true;
|
|
90
123
|
unsubscribe();
|
|
124
|
+
unwake();
|
|
91
125
|
if (timer !== undefined)
|
|
92
126
|
clearTimeout(timer);
|
|
127
|
+
if (beat !== undefined)
|
|
128
|
+
clearInterval(beat);
|
|
93
129
|
signal?.removeEventListener('abort', onAbort);
|
|
94
130
|
resolve({
|
|
95
131
|
jobs: collect(port, targets),
|
|
96
|
-
terminal: reason === 'terminal' ||
|
|
132
|
+
terminal: reason === 'terminal' || isDone(port, targets, mode),
|
|
97
133
|
waitedMs: now() - startedAt,
|
|
98
134
|
timedOut: reason === 'timeout',
|
|
99
135
|
aborted: reason === 'abort',
|
|
136
|
+
woken: reason === 'wake',
|
|
100
137
|
pages: readPages(port, targets, req.since),
|
|
101
138
|
});
|
|
102
139
|
};
|
|
@@ -105,12 +142,38 @@ export async function waitForJob(port, req, now = Date.now) {
|
|
|
105
142
|
return;
|
|
106
143
|
if (!targets.includes(event.service.id))
|
|
107
144
|
return;
|
|
108
|
-
if (
|
|
145
|
+
if (isDone(port, targets, mode))
|
|
109
146
|
finish('terminal');
|
|
110
147
|
});
|
|
148
|
+
// The user spoke. Edge-triggered: only a notification after this line counts.
|
|
149
|
+
unwake = req.wakeOn?.subscribe(() => finish('wake')) ?? (() => { });
|
|
150
|
+
if (done) {
|
|
151
|
+
// A source that notifies synchronously inside `subscribe` already settled us.
|
|
152
|
+
unwake();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
111
155
|
// RULE 2: `unref`, so a pending wait never keeps a quitting process alive.
|
|
112
156
|
timer = setTimeout(() => finish('timeout'), Math.max(0, req.timeoutMs));
|
|
113
157
|
timer.unref?.();
|
|
158
|
+
// RULE 5. Also the view's 1 Hz clock. A throwing `onBeat` is a renderer bug and
|
|
159
|
+
// must never kill a 20-minute wait.
|
|
160
|
+
if (req.heartbeat) {
|
|
161
|
+
const { everyMs, onBeat } = req.heartbeat;
|
|
162
|
+
beat = setInterval(() => {
|
|
163
|
+
const current = now();
|
|
164
|
+
if (current - startedAt >= req.timeoutMs) {
|
|
165
|
+
finish('timeout');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
onBeat(current);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Deliberately swallowed: see above.
|
|
173
|
+
}
|
|
174
|
+
}, Math.max(1, everyMs));
|
|
175
|
+
beat.unref?.();
|
|
176
|
+
}
|
|
114
177
|
onAbort = () => finish('abort');
|
|
115
178
|
// `{ once: true }` AND the explicit removal in `finish`: the option alone
|
|
116
179
|
// leaves the listener registered on a signal that outlives this wait.
|
package/dist/session/persist.js
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
9
9
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
10
10
|
import { getSessionsDir } from '../config/store.js';
|
|
11
11
|
import { stripEphemeralRuns } from '../team/settled.js';
|
|
12
|
+
import { sanitizeInvocationCard } from '../agent/invocation-card.js';
|
|
12
13
|
import { validateSession } from './validate-session.js';
|
|
13
14
|
import { validateCompactionIdentity, verifyCompactionIdentity, } from '../compaction/memory-identity.js';
|
|
14
15
|
import { getLogger } from '../logging/logger.js';
|
|
@@ -59,8 +60,20 @@ function defaultStamp() {
|
|
|
59
60
|
*/
|
|
60
61
|
function stripLiveToolOutput(entries) {
|
|
61
62
|
return entries.map((e) => e.kind === 'tool' &&
|
|
62
|
-
(e.live !== undefined ||
|
|
63
|
-
|
|
63
|
+
(e.live !== undefined ||
|
|
64
|
+
e.liveSeq !== undefined ||
|
|
65
|
+
e.lastOutputAt !== undefined ||
|
|
66
|
+
e.wait !== undefined ||
|
|
67
|
+
e.waitSeq !== undefined)
|
|
68
|
+
? {
|
|
69
|
+
...e,
|
|
70
|
+
live: undefined,
|
|
71
|
+
liveSeq: undefined,
|
|
72
|
+
lastOutputAt: undefined,
|
|
73
|
+
// `wait` / `waitSeq` die with `live` (extended-job-wait I-9).
|
|
74
|
+
wait: undefined,
|
|
75
|
+
waitSeq: undefined,
|
|
76
|
+
}
|
|
64
77
|
: e);
|
|
65
78
|
}
|
|
66
79
|
/**
|
|
@@ -145,6 +158,11 @@ export function saveSession(filePath, data) {
|
|
|
145
158
|
*/
|
|
146
159
|
export function normalizeLoadedEntries(entries) {
|
|
147
160
|
return entries.map((entry) => {
|
|
161
|
+
// A runtime-only wait view that reached disk anyway (an older or hand-edited
|
|
162
|
+
// file) describes a wait that died with the session: drop it (I-9).
|
|
163
|
+
if (entry.kind === 'tool' && (entry.wait !== undefined || entry.waitSeq !== undefined)) {
|
|
164
|
+
return { ...entry, wait: undefined, waitSeq: undefined };
|
|
165
|
+
}
|
|
148
166
|
if (entry.kind === 'team' && entry.active) {
|
|
149
167
|
return { ...entry, active: false, aborted: true };
|
|
150
168
|
}
|
|
@@ -227,6 +245,15 @@ export function normalizeLoadedEntries(entries) {
|
|
|
227
245
|
// THE ID IS KEPT: `restoreEntries` splices these positions into the
|
|
228
246
|
// live transcript, and a fresh id here would collide with the next
|
|
229
247
|
// `e<n>` the reducer allocates.
|
|
248
|
+
if (entry.kind === 'user' && 'invocation' in entry) {
|
|
249
|
+
// THE EIGHTH CLAUSE (tui-invocation-cards I-7). A session file is user-editable
|
|
250
|
+
// and `shape()` ignores undeclared fields, so a card arrives unchecked. Repair
|
|
251
|
+
// what can be repaired; a card that cannot be is dropped and the entry degrades
|
|
252
|
+
// to a plain user message - a corrupt card must never cost the session.
|
|
253
|
+
const { invocation: stored, ...plain } = entry;
|
|
254
|
+
const card = sanitizeInvocationCard(stored);
|
|
255
|
+
return card ? { ...plain, invocation: card } : plain;
|
|
256
|
+
}
|
|
230
257
|
if (entry.kind === 'queued') {
|
|
231
258
|
const downgraded = {
|
|
232
259
|
id: entry.id,
|
|
@@ -145,14 +145,16 @@ const subagent = shape({
|
|
|
145
145
|
retry: optional(shape({ attempt: number, maxRetries: number })),
|
|
146
146
|
});
|
|
147
147
|
const ENTRY_FIELDS = {
|
|
148
|
-
|
|
148
|
+
// `invocation` is declared for self-description only: `shape()` ignores undeclared
|
|
149
|
+
// fields, and `normalizeLoadedEntries` repairs or drops a bad card (tui-invocation-cards).
|
|
150
|
+
user: { text: string, invocation: optional(() => { }) },
|
|
149
151
|
// A GOVERNANCE CARD IS HISTORY, so it is validated LENIENTLY (governance-mode 10.4):
|
|
150
152
|
// unknown fields are ignored (`shape` only reads the declared ones) and `card` is not
|
|
151
153
|
// shape-checked here at all - `sanitizeGovernanceEntries` below repairs a card it can
|
|
152
154
|
// and degrades one it cannot to a one-line notice, so a corrupt footnote never costs
|
|
153
155
|
// the session. `at` is advisory.
|
|
154
156
|
governance: { at: optional(number), card: () => { } },
|
|
155
|
-
queued: { text: string, queueId: optional(nonempty) },
|
|
157
|
+
queued: { text: string, queueId: optional(nonempty), invocation: optional(() => { }) },
|
|
156
158
|
notice: { text: string, level: enumeration('info', 'warn', 'error') },
|
|
157
159
|
assistant: {
|
|
158
160
|
text: string, thinking: optional(string), thinkingOpen: boolean, streaming: boolean,
|
|
@@ -164,6 +166,9 @@ const ENTRY_FIELDS = {
|
|
|
164
166
|
status: enumeration('pending', 'running', 'done', 'error'), preview: optional(string),
|
|
165
167
|
durationMs: optional(number), isError: optional(boolean), patch: optional(patch),
|
|
166
168
|
live: optional(array(string)), liveSeq: optional(number), lastOutputAt: optional(number),
|
|
169
|
+
// Runtime-only (extended-job-wait I-9): never written, but tolerated on read so a
|
|
170
|
+
// session saved mid-wait by any path still resumes; `normalizeLoadedEntries` drops it.
|
|
171
|
+
wait: optional(object), waitSeq: optional(number),
|
|
167
172
|
},
|
|
168
173
|
team: { dispatchId: string, requested: number, runs: array(subagent), aborted: boolean,
|
|
169
174
|
durationMs: optional(number), active: boolean },
|
|
@@ -106,6 +106,19 @@ export interface ForcedPlan {
|
|
|
106
106
|
* can apply, because only it can read the live config.
|
|
107
107
|
*/
|
|
108
108
|
export declare function planForced(args: SubagentCommandArgs, team: TeamConfig): ForcedPlan;
|
|
109
|
+
/**
|
|
110
|
+
* The parenthesised disclosure sentence of the plan notice - why the fan-out is not what
|
|
111
|
+
* the user typed - or `null` when nothing was narrowed.
|
|
112
|
+
*
|
|
113
|
+
* SHARED BY `planNotice` AND `forcedCardNote` (tui-invocation-cards RV-4): the notice and
|
|
114
|
+
* the transcript card must word the same fact the same way, so there is exactly one
|
|
115
|
+
* place that decides it.
|
|
116
|
+
*
|
|
117
|
+
* TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may carry and
|
|
118
|
+
* `team.maxSubagents` bounds what may be dispatched; naming the wrong one sends the user
|
|
119
|
+
* to change a number that is not the reason.
|
|
120
|
+
*/
|
|
121
|
+
export declare function clampDisclosure(planned: ForcedPlan, team: TeamConfig): string | null;
|
|
109
122
|
/**
|
|
110
123
|
* The one line that says what the NEXT turn will do - a plan, not a result.
|
|
111
124
|
*
|
|
@@ -114,3 +127,9 @@ export declare function planForced(args: SubagentCommandArgs, team: TeamConfig):
|
|
|
114
127
|
* said "4 subagents started" would be a promise the command cannot keep.
|
|
115
128
|
*/
|
|
116
129
|
export declare function planNotice(planned: ForcedPlan, team: TeamConfig): string;
|
|
130
|
+
/**
|
|
131
|
+
* The card's `note`: the same disclosure, as a sentence. `undefined` when nothing was
|
|
132
|
+
* narrowed - the "Esc stops the turn" follow-up is an operating hint the status bar
|
|
133
|
+
* carries, not a fact about this invocation.
|
|
134
|
+
*/
|
|
135
|
+
export declare function forcedCardNote(planned: ForcedPlan, team: TeamConfig): string | undefined;
|
package/dist/team/forced-mode.js
CHANGED
|
@@ -123,34 +123,56 @@ export function planForced(args, team) {
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
* The
|
|
126
|
+
* The parenthesised disclosure sentence of the plan notice - why the fan-out is not what
|
|
127
|
+
* the user typed - or `null` when nothing was narrowed.
|
|
127
128
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
129
|
+
* SHARED BY `planNotice` AND `forcedCardNote` (tui-invocation-cards RV-4): the notice and
|
|
130
|
+
* the transcript card must word the same fact the same way, so there is exactly one
|
|
131
|
+
* place that decides it.
|
|
132
|
+
*
|
|
133
|
+
* TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may carry and
|
|
134
|
+
* `team.maxSubagents` bounds what may be dispatched; naming the wrong one sends the user
|
|
135
|
+
* to change a number that is not the reason.
|
|
131
136
|
*/
|
|
132
|
-
export function
|
|
133
|
-
const noun = planned.count === 1 ? 'subagent' : 'subagents';
|
|
134
|
-
const head = `Forcing a fan-out of ${planned.count} ${noun}`;
|
|
135
|
-
const followUp = `${head} (max ${team.maxSubagents}) - the next turn dispatches them. `
|
|
136
|
-
+ 'Esc stops the turn.';
|
|
137
|
-
// TWO CEILINGS, TWO SENTENCES. `forcedPartsMax` bounds what one argument may
|
|
138
|
-
// carry and `team.maxSubagents` bounds what may be dispatched; naming the wrong
|
|
139
|
-
// one sends the user to change a number that is not the reason.
|
|
137
|
+
export function clampDisclosure(planned, team) {
|
|
140
138
|
if (planned.parts.length > 0) {
|
|
141
139
|
if (planned.parts.length < planned.partsAdmitted) {
|
|
142
|
-
return
|
|
143
|
-
+ `team.maxSubagents is ${team.maxSubagents}).`;
|
|
140
|
+
return `asked for ${planned.partsRequested} parts; team.maxSubagents is ${team.maxSubagents}`;
|
|
144
141
|
}
|
|
145
142
|
if (planned.partsAdmitted < planned.partsRequested) {
|
|
146
|
-
return
|
|
147
|
-
+ `the first ${planned.parts.length} are used).`;
|
|
143
|
+
return `asked for ${planned.partsRequested} parts, the first ${planned.parts.length} are used`;
|
|
148
144
|
}
|
|
149
|
-
return
|
|
145
|
+
return null;
|
|
150
146
|
}
|
|
151
147
|
if (planned.requested > planned.count) {
|
|
152
|
-
return
|
|
153
|
-
+ `team.maxSubagents is ${team.maxSubagents}).`;
|
|
148
|
+
return `asked for ${planned.requested}; team.maxSubagents is ${team.maxSubagents}`;
|
|
154
149
|
}
|
|
155
|
-
return
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The one line that says what the NEXT turn will do - a plan, not a result.
|
|
154
|
+
*
|
|
155
|
+
* IT IS PRINTED BEFORE THE TURN IS SUBMITTED and it never claims children
|
|
156
|
+
* started: forcing is instruction-level for the first turn, and a notice that
|
|
157
|
+
* said "4 subagents started" would be a promise the command cannot keep.
|
|
158
|
+
*/
|
|
159
|
+
export function planNotice(planned, team) {
|
|
160
|
+
const noun = planned.count === 1 ? 'subagent' : 'subagents';
|
|
161
|
+
const head = `Forcing a fan-out of ${planned.count} ${noun}`;
|
|
162
|
+
const disclosure = clampDisclosure(planned, team);
|
|
163
|
+
if (disclosure !== null)
|
|
164
|
+
return `${head} (${disclosure}).`;
|
|
165
|
+
return `${head} (max ${team.maxSubagents}) - the next turn dispatches them. `
|
|
166
|
+
+ 'Esc stops the turn.';
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The card's `note`: the same disclosure, as a sentence. `undefined` when nothing was
|
|
170
|
+
* narrowed - the "Esc stops the turn" follow-up is an operating hint the status bar
|
|
171
|
+
* carries, not a fact about this invocation.
|
|
172
|
+
*/
|
|
173
|
+
export function forcedCardNote(planned, team) {
|
|
174
|
+
const disclosure = clampDisclosure(planned, team);
|
|
175
|
+
if (disclosure === null)
|
|
176
|
+
return undefined;
|
|
177
|
+
return `${disclosure.charAt(0).toUpperCase()}${disclosure.slice(1)}.`;
|
|
156
178
|
}
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
import { type AgentTool } from '@aragon-agent/core';
|
|
19
19
|
import { type JobNotice } from '../proc/notices.js';
|
|
20
20
|
import type { JobKind, ProcSupervisorPort, ServiceStatus } from '../proc/types.js';
|
|
21
|
+
import { type WakeSource } from '../proc/wait.js';
|
|
22
|
+
import { type WaitListener } from '../proc/wait-view.js';
|
|
21
23
|
export interface JobToolDeps {
|
|
22
24
|
supervisor: ProcSupervisorPort;
|
|
23
25
|
/** `cfg.jobs.waitSeconds`, read LIVE (same rule as `startupSettleMs`). */
|
|
24
26
|
waitSeconds: () => number;
|
|
25
27
|
/** `cfg.jobs.stallSeconds`, read LIVE; only the `[stalled]` sentence uses it. */
|
|
26
28
|
stallSeconds: () => number;
|
|
27
|
-
/** `cfg.toolTimeoutMs`, read LIVE: I-4's second convergence. */
|
|
28
|
-
toolTimeoutMs: () => number;
|
|
29
29
|
/** `PROC_LIMITS.maxConsecutiveWaits`. */
|
|
30
30
|
maxConsecutiveWaits: number;
|
|
31
31
|
/**
|
|
@@ -40,6 +40,17 @@ export interface JobToolDeps {
|
|
|
40
40
|
kind: JobKind;
|
|
41
41
|
status: ServiceStatus;
|
|
42
42
|
} | undefined;
|
|
43
|
+
/** Live view of the wait (`begin` / 1 Hz `update` / `end`); absent for a sub-agent. */
|
|
44
|
+
onWait?: WaitListener;
|
|
45
|
+
/**
|
|
46
|
+
* Runs the whole wait with the idle watchdog paused (`controller.withPausedWatchdog`).
|
|
47
|
+
* Absent in a sub-agent or a fixture: the wait then runs unwrapped, as before.
|
|
48
|
+
*/
|
|
49
|
+
holdWatchdog?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
50
|
+
/** Ends a wait when the user sends a message (edge-triggered; see `proc/wait.ts`). */
|
|
51
|
+
attention?: WakeSource;
|
|
52
|
+
/** Test seam; defaults to `Date.now`. */
|
|
53
|
+
now?: () => number;
|
|
43
54
|
}
|
|
44
55
|
export declare function makeJobWait(deps: JobToolDeps): AgentTool;
|
|
45
56
|
export declare function makeJobList(deps: JobToolDeps): AgentTool;
|