@aragon-agent/cli 0.6.22 → 0.6.24
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/README.md +20 -2
- package/dist/agent/controller.d.ts +11 -0
- package/dist/agent/controller.js +42 -4
- package/dist/agent/model-profile-settings.js +29 -2
- package/dist/agent/reducer.js +8 -6
- package/dist/commands/builtins.js +87 -3
- package/dist/compaction/child.d.ts +15 -0
- package/dist/compaction/child.js +14 -3
- package/dist/compaction/compactor.js +1 -0
- package/dist/compaction/context-command.js +10 -1
- package/dist/compaction/meter.d.ts +9 -0
- package/dist/compaction/meter.js +16 -1
- package/dist/compaction/pressure.d.ts +7 -0
- package/dist/compaction/pressure.js +5 -0
- package/dist/compaction/types.d.ts +11 -0
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +12 -0
- package/dist/config/load.js +66 -9
- package/dist/config/model-profile-resolution.d.ts +8 -0
- package/dist/config/model-profile-resolution.js +13 -6
- package/dist/config/model-profile-store.d.ts +4 -2
- package/dist/config/model-profiles.d.ts +13 -0
- package/dist/config/model-profiles.js +18 -2
- package/dist/config/schema.d.ts +86 -0
- package/dist/config/schema.js +74 -0
- package/dist/team/model.d.ts +88 -0
- package/dist/team/model.js +136 -0
- package/dist/team/overseer.d.ts +5 -3
- package/dist/team/overseer.js +15 -4
- package/dist/team/report.d.ts +11 -0
- package/dist/team/report.js +24 -1
- package/dist/team/runtime.js +20 -3
- package/dist/team/subagent.d.ts +10 -0
- package/dist/team/subagent.js +57 -27
- package/dist/team/task-tool.d.ts +25 -6
- package/dist/team/task-tool.js +15 -9
- package/dist/team/types.d.ts +4 -3
- package/dist/ui/App.js +5 -0
- package/dist/ui/overlays/ModelProfileEditor.js +18 -4
- package/dist/ui/overlays/ModelProfilePicker.d.ts +14 -2
- package/dist/ui/overlays/ModelProfilePicker.js +51 -10
- package/dist/ui/overlays/QuestionOverlay.js +47 -6
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -0
- package/dist/ui/overlays/SettingsScreen.js +119 -17
- package/dist/ui/overlays/model-profile-state.d.ts +53 -4
- package/dist/ui/overlays/model-profile-state.js +111 -14
- package/package.json +2 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `resolveSubagentModel` — THE single authority on which model a team child
|
|
3
|
+
* runs (subagent-model-and-question-controls §3.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* EVERY CONSUMER READS THE CHILD'S MODEL THROUGH THIS FUNCTION AND NOWHERE
|
|
8
|
+
* ELSE — `createSubagent`, the `task` report header, the settings root row and
|
|
9
|
+
* `/team model` — following the `resolveFastTier` precedent one directory over:
|
|
10
|
+
* four call sites each recomputing "which model do children run on?" from raw
|
|
11
|
+
* config keys is exactly how the report ends up naming a model the children
|
|
12
|
+
* silently fell back from.
|
|
13
|
+
*
|
|
14
|
+
* IT IS RE-RUN ON EVERY DISPATCH AND EVERY RENDER, NOT ONCE (rule 8 / RV-3 of
|
|
15
|
+
* the fast tier, inherited here): `team.model`, the fast tier and the profile
|
|
16
|
+
* library are all LIVE keys a settings save, `/fast off` or `/team model` can
|
|
17
|
+
* rewrite mid-session. A resolution captured at construction would be a
|
|
18
|
+
* resolution describing a session that no longer exists.
|
|
19
|
+
*
|
|
20
|
+
* A CHILD IS NEVER LEFT WITHOUT A MODEL (D-4). Every failure mode — fast tier
|
|
21
|
+
* off, profile deleted, profile key missing — degrades to the MAIN connection
|
|
22
|
+
* with a `fallbackReason`, so a dispatch cannot fail because its model setting
|
|
23
|
+
* went stale. The reason is disclosed, never swallowed: the report header and
|
|
24
|
+
* the settings summary line both render it.
|
|
25
|
+
*/
|
|
26
|
+
import { resolveFastTier } from '../fast/resolve.js';
|
|
27
|
+
function mainRef(config) {
|
|
28
|
+
return {
|
|
29
|
+
providerId: config.provider,
|
|
30
|
+
modelId: config.model,
|
|
31
|
+
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function fallbackToMain(config, mode, fallbackReason) {
|
|
35
|
+
// THE ALWAYS-USABLE ANSWER (D-4): whatever the user asked for that could not
|
|
36
|
+
// resolve, the caller still gets a working connection — the same provider,
|
|
37
|
+
// model, base URL and thinking level the lead itself runs on.
|
|
38
|
+
return {
|
|
39
|
+
mode,
|
|
40
|
+
effective: 'main',
|
|
41
|
+
ref: mainRef(config),
|
|
42
|
+
thinkingLevel: config.thinkingLevel,
|
|
43
|
+
keyRole: 'main',
|
|
44
|
+
profile: null,
|
|
45
|
+
fallbackReason,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the model every child of the NEXT dispatch will run on.
|
|
50
|
+
*
|
|
51
|
+
* Pure, synchronous, no IO — testable as a matrix over config fixtures.
|
|
52
|
+
*
|
|
53
|
+
* @param config The live effective config. Read fresh; never snapshot it.
|
|
54
|
+
* @param hasKey `AgentController.hasApiKey`, which already resolves the
|
|
55
|
+
* config-file key, the provider environment variable and the one-shot
|
|
56
|
+
* `--api-key` override. Injected rather than reimplemented for the reason
|
|
57
|
+
* `resolveFastTier` records: a second key resolver is a second answer to
|
|
58
|
+
* "why does it say no key?".
|
|
59
|
+
*/
|
|
60
|
+
export function resolveSubagentModel(config, hasKey) {
|
|
61
|
+
// `clampTeamConfig` guarantees the enum on every read and write path, so the
|
|
62
|
+
// default `'main'` here is belt-and-braces rather than a real branch.
|
|
63
|
+
const mode = config.team.model ?? 'main';
|
|
64
|
+
if (mode === 'fast') {
|
|
65
|
+
const tier = resolveFastTier(config, hasKey);
|
|
66
|
+
if (tier.ok) {
|
|
67
|
+
return {
|
|
68
|
+
mode,
|
|
69
|
+
effective: 'fast',
|
|
70
|
+
ref: tier.ref,
|
|
71
|
+
thinkingLevel: config.fast.thinkingLevel,
|
|
72
|
+
keyRole: 'fast',
|
|
73
|
+
profile: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return fallbackToMain(config, mode, 'fast_unavailable');
|
|
77
|
+
}
|
|
78
|
+
if (mode === 'profile') {
|
|
79
|
+
// A missing library, a missing id and a dangling id are one outcome
|
|
80
|
+
// (D-4): degrade to main and say so. Reference EXISTENCE is deliberately
|
|
81
|
+
// not validated in `clampTeamConfig` — resolution is where it belongs.
|
|
82
|
+
const id = config.team.modelProfileId;
|
|
83
|
+
const entry = id
|
|
84
|
+
? config.modelProfiles?.entries.find((candidate) => candidate.id === id)
|
|
85
|
+
: undefined;
|
|
86
|
+
if (!entry)
|
|
87
|
+
return fallbackToMain(config, mode, 'profile_missing');
|
|
88
|
+
// The dedicated entry key wins; without one the shared layer must have a
|
|
89
|
+
// key for the entry's provider, or the child could not authenticate at all.
|
|
90
|
+
if (!entry.apiKey && !hasKey(entry.provider, 'main')) {
|
|
91
|
+
return fallbackToMain(config, mode, 'profile_no_key');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
mode,
|
|
95
|
+
effective: 'profile',
|
|
96
|
+
ref: {
|
|
97
|
+
providerId: entry.provider,
|
|
98
|
+
modelId: entry.model,
|
|
99
|
+
...(entry.baseUrl ? { baseUrl: entry.baseUrl } : {}),
|
|
100
|
+
},
|
|
101
|
+
thinkingLevel: config.thinkingLevel,
|
|
102
|
+
keyRole: 'main',
|
|
103
|
+
profile: entry,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
mode: 'main',
|
|
108
|
+
effective: 'main',
|
|
109
|
+
ref: mainRef(config),
|
|
110
|
+
thinkingLevel: config.thinkingLevel,
|
|
111
|
+
keyRole: 'main',
|
|
112
|
+
profile: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The key a CHILD resolves for a provider, profile-aware (§3.3 / I-4).
|
|
117
|
+
*
|
|
118
|
+
* A profile's dedicated key is visible ONLY here: it never reaches env,
|
|
119
|
+
* `config.apiKeys` or a log line. While the resolution's profile is bound and
|
|
120
|
+
* the provider matches it, the entry key takes priority over the shared layer;
|
|
121
|
+
* every other provider (and every non-profile mode) resolves through the
|
|
122
|
+
* lead's own resolver at `resolution.keyRole` — which is what keeps a `fast`
|
|
123
|
+
* child entitled to the fast-role key and a `main` child to the main one.
|
|
124
|
+
*
|
|
125
|
+
* @param config The live config (carried for parity with the resolver pair).
|
|
126
|
+
* @param resolution A fresh `resolveSubagentModel` output.
|
|
127
|
+
* @param providerId The provider the child needs a key for.
|
|
128
|
+
* @param shared The lead-layer resolver (`controller.resolveKey`).
|
|
129
|
+
*/
|
|
130
|
+
export function resolveSubagentKey(config, resolution, providerId, shared) {
|
|
131
|
+
void config; // parity parameter: the pair reads one config, the key does not need it
|
|
132
|
+
if (resolution.profile && providerId === resolution.ref.providerId) {
|
|
133
|
+
return resolution.profile.apiKey ?? shared(providerId, 'main');
|
|
134
|
+
}
|
|
135
|
+
return shared(providerId, resolution.keyRole);
|
|
136
|
+
}
|
package/dist/team/overseer.d.ts
CHANGED
|
@@ -49,8 +49,10 @@ import type { OverseerAction, OverseerDecision, SubagentRun, SubagentSpec } from
|
|
|
49
49
|
* Bumped whenever either supervisor protocol prompt changes (D-9), so a
|
|
50
50
|
* behaviour report can be tied to a prompt revision with one grep.
|
|
51
51
|
* `TEAM_BLOCK_VERSION` covers only the LEAD's `<team>` block, not these.
|
|
52
|
+
*
|
|
53
|
+
* v3 adds the turn-cap trigger to the single-inspection prompt (F-8 A'2).
|
|
52
54
|
*/
|
|
53
|
-
export declare const OVERSEER_PROMPT_VERSION = "
|
|
55
|
+
export declare const OVERSEER_PROMPT_VERSION = "v3-2026-10";
|
|
54
56
|
/** One remembered decision, so the second look knows the first happened. */
|
|
55
57
|
export interface OverseerMemoryEntry {
|
|
56
58
|
/** Seconds into the dispatch. */
|
|
@@ -63,7 +65,7 @@ export interface ChildDigestInput {
|
|
|
63
65
|
run: SubagentRun;
|
|
64
66
|
/** False only in the post-mortem inspection, after the child settled. */
|
|
65
67
|
childAlive: boolean;
|
|
66
|
-
trigger: 'silence' | 'clock' | 'postmortem';
|
|
68
|
+
trigger: 'silence' | 'clock' | 'turncap' | 'postmortem';
|
|
67
69
|
/** The child's LIVE history (`SubagentAgentLike.state.messages`). */
|
|
68
70
|
messages: readonly Message[];
|
|
69
71
|
memory: readonly OverseerMemoryEntry[];
|
|
@@ -203,7 +205,7 @@ export declare function createFastOverseerProvider(deps: FastOverseerDeps): Over
|
|
|
203
205
|
export interface OverseerInspectRequest {
|
|
204
206
|
run: SubagentRun;
|
|
205
207
|
messages: readonly Message[];
|
|
206
|
-
trigger: 'silence' | 'clock' | 'postmortem';
|
|
208
|
+
trigger: 'silence' | 'clock' | 'turncap' | 'postmortem';
|
|
207
209
|
childAlive: boolean;
|
|
208
210
|
}
|
|
209
211
|
export interface TeamOverseerOptions {
|
package/dist/team/overseer.js
CHANGED
|
@@ -50,8 +50,10 @@ import { TEAM_LIMITS } from './limits.js';
|
|
|
50
50
|
* Bumped whenever either supervisor protocol prompt changes (D-9), so a
|
|
51
51
|
* behaviour report can be tied to a prompt revision with one grep.
|
|
52
52
|
* `TEAM_BLOCK_VERSION` covers only the LEAD's `<team>` block, not these.
|
|
53
|
+
*
|
|
54
|
+
* v3 adds the turn-cap trigger to the single-inspection prompt (F-8 A'2).
|
|
53
55
|
*/
|
|
54
|
-
export const OVERSEER_PROMPT_VERSION = '
|
|
56
|
+
export const OVERSEER_PROMPT_VERSION = 'v3-2026-10';
|
|
55
57
|
/** How many trailing history messages the digest carries. */
|
|
56
58
|
const DIGEST_TAIL_MESSAGES = 8;
|
|
57
59
|
/** Per-message text clamp inside the digest. */
|
|
@@ -78,6 +80,13 @@ function messageText(message) {
|
|
|
78
80
|
function clampChars(text, max) {
|
|
79
81
|
return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
80
82
|
}
|
|
83
|
+
/** One line per trigger, as the digest names it (a union VALUE, never prose). */
|
|
84
|
+
const TRIGGER_LABELS = {
|
|
85
|
+
silence: 'event silence (suspected stall)',
|
|
86
|
+
clock: 'wall-clock check',
|
|
87
|
+
turncap: 'turn cap reached (inspection trigger, not a stop)',
|
|
88
|
+
postmortem: 'post-mortem (child already ended)',
|
|
89
|
+
};
|
|
81
90
|
/**
|
|
82
91
|
* Build the bounded text one inspection reads.
|
|
83
92
|
*
|
|
@@ -91,7 +100,7 @@ export function buildChildDigest(input) {
|
|
|
91
100
|
const { run, childAlive, trigger, messages, memory, elapsedSec } = input;
|
|
92
101
|
const lines = [];
|
|
93
102
|
lines.push(`child: ${run.label} "${run.description}"`);
|
|
94
|
-
lines.push(`trigger: ${trigger
|
|
103
|
+
lines.push(`trigger: ${TRIGGER_LABELS[trigger]}`);
|
|
95
104
|
lines.push(`elapsed: ${Math.round(elapsedSec)}s, alive: ${childAlive ? 'yes' : 'no'}`);
|
|
96
105
|
lines.push(`phase: ${run.phase}, turns: ${run.turns}, toolCalls: ${run.toolCalls}` +
|
|
97
106
|
(run.lastTool ? `, lastTool: ${run.lastTool}` : ''));
|
|
@@ -270,8 +279,10 @@ export function parseOverseerBatchText(text) {
|
|
|
270
279
|
}
|
|
271
280
|
const OVERSEER_SYSTEM_PROMPT = [
|
|
272
281
|
'You are the supervisor of a team of subagents. You are shown ONE subagent',
|
|
273
|
-
'that crossed a
|
|
274
|
-
'decide what to do with it.
|
|
282
|
+
'that crossed a supervision trigger (event silence, a wall-clock check, or',
|
|
283
|
+
'the configured turn cap) and must decide what to do with it. Crossing a',
|
|
284
|
+
'ceiling does NOT stop the child - your decision does. Answer with ONE JSON',
|
|
285
|
+
'object and nothing else:',
|
|
275
286
|
'{"action": "wait" | "nudge" | "replace" | "abandon", "reason": "<= 200 chars",',
|
|
276
287
|
' "guidance": "<= 1200 chars, nudge only", "nextCheckMs": <ms, wait only>}.',
|
|
277
288
|
'wait: the child looks healthy or is legitimately blocked (API backoff,',
|
package/dist/team/report.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* because of one HTTP 429.
|
|
22
22
|
*/
|
|
23
23
|
import type { ModelCost } from '@aragon-agent/core';
|
|
24
|
+
import type { SubagentModelResolution } from './model.js';
|
|
24
25
|
import type { DispatchOutcome, SubagentRun } from './types.js';
|
|
25
26
|
export interface ReportOptions {
|
|
26
27
|
/** The lead's model cost table, so the header can state real spend (R-5).
|
|
@@ -37,6 +38,16 @@ export interface ReportOptions {
|
|
|
37
38
|
* rule the fast tier already follows everywhere else).
|
|
38
39
|
*/
|
|
39
40
|
fastCost?: ModelCost;
|
|
41
|
+
/**
|
|
42
|
+
* The dispatch-time subagent model resolution (subagent-model §3.4).
|
|
43
|
+
*
|
|
44
|
+
* ABSENT under the default `team.model: 'main'` mode is what keeps the
|
|
45
|
+
* parity report byte-identical (I-2): the disclosure line exists only when
|
|
46
|
+
* children did NOT run the main model, and a resolution that degraded
|
|
47
|
+
* (fast tier off, profile deleted, profile key missing) says so instead of
|
|
48
|
+
* silently renaming the model the children fell back to (D-4).
|
|
49
|
+
*/
|
|
50
|
+
subagentModel?: SubagentModelResolution;
|
|
40
51
|
}
|
|
41
52
|
/**
|
|
42
53
|
* Cut `text` to at most `maxBytes` WITHOUT splitting a UTF-8 sequence.
|
package/dist/team/report.js
CHANGED
|
@@ -106,8 +106,31 @@ function buildHeader(outcome, options) {
|
|
|
106
106
|
`(${counts}) in ${formatDuration(durationMs)}.`
|
|
107
107
|
: `Team dispatch: ${outcome.runs.length} of ${outcome.runs.length} subagents finished ` +
|
|
108
108
|
`(${counts}) in ${formatDuration(durationMs)}.`);
|
|
109
|
+
// THE PRICE OF THE MODEL CHILDREN ACTUALLY RAN (subagent-model §3.4). The
|
|
110
|
+
// default main flow always supplies a table, so the ternary's last branch
|
|
111
|
+
// is the legacy byte-identical shape; an UNPRICED fast or profile child
|
|
112
|
+
// model says `pricing unknown` - unknown pricing is not zero pricing, and
|
|
113
|
+
// `$0.00` on a dispatch that is spending money is the C-11 / RV-4 lie.
|
|
114
|
+
const spend = options.cost
|
|
115
|
+
? formatCost(computeCost(outcome.usage, options.cost))
|
|
116
|
+
: options.subagentModel && options.subagentModel.effective !== 'main'
|
|
117
|
+
? 'pricing unknown'
|
|
118
|
+
: formatCost(computeCost(outcome.usage, options.cost));
|
|
109
119
|
lines.push(`Tokens: in ${formatTokens(outcome.usage.inputTokens)}, out ` +
|
|
110
|
-
`${formatTokens(outcome.usage.outputTokens)}. Cost: ${
|
|
120
|
+
`${formatTokens(outcome.usage.outputTokens)}. Cost: ${spend}.`);
|
|
121
|
+
// THE MODEL DISCLOSURE LINE (D-4), immediately after the spend it explains.
|
|
122
|
+
// Rendered only when children did not run the main model, so the default
|
|
123
|
+
// report stays byte-identical (I-2) and a degraded request says WHICH mode
|
|
124
|
+
// was asked for and WHY it fell back rather than quietly renaming it.
|
|
125
|
+
const subagent = options.subagentModel;
|
|
126
|
+
if (subagent && subagent.effective !== 'main') {
|
|
127
|
+
lines.push(`Children ran on ${subagent.ref.providerId}:${subagent.ref.modelId} ` +
|
|
128
|
+
`(subagent model: ${subagent.effective}).`);
|
|
129
|
+
}
|
|
130
|
+
else if (subagent && subagent.fallbackReason) {
|
|
131
|
+
lines.push(`Subagent model ${subagent.mode} unavailable (${subagent.fallbackReason}); ` +
|
|
132
|
+
`children ran on the main model.`);
|
|
133
|
+
}
|
|
111
134
|
for (const conflict of findFileConflicts(outcome.runs)) {
|
|
112
135
|
const who = conflict.labels.map((l) => `"${l}"`).join(' and ');
|
|
113
136
|
lines.push(`WARNING: agents ${who} both wrote ${conflict.path} - review before trusting either.`);
|
package/dist/team/runtime.js
CHANGED
|
@@ -488,6 +488,18 @@ export class TeamRuntime {
|
|
|
488
488
|
// ONE KICK PER REAL AGENT EVENT, unthrottled (I-OV3): the silence
|
|
489
489
|
// window resets on evidence, not on a coalesced UI frame.
|
|
490
490
|
onActivity: () => this.supervision?.kick(spec.label),
|
|
491
|
+
// A'2: under supervision the turn cap is ONE inspection, fired from the
|
|
492
|
+
// child's own `turn_end` at the crossing turn. Async-and-forgotten
|
|
493
|
+
// exactly like the silence timer's `onFire`: nothing in the child's
|
|
494
|
+
// event path may block or throw. ABSENT in the legacy regime, which
|
|
495
|
+
// keeps the hard stop - the hook's presence IS the gate.
|
|
496
|
+
...(this.overseer !== null
|
|
497
|
+
? {
|
|
498
|
+
onTurnCapReached: (label) => {
|
|
499
|
+
void this.inspectAndApply(dispatchId, label, 'turncap');
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
: {}),
|
|
491
503
|
});
|
|
492
504
|
// ASSIGNMENT, not a declaration: a `const` here would shadow the `let` above
|
|
493
505
|
// the bus, `canSend` would see the empty array forever, and every peer would
|
|
@@ -520,10 +532,15 @@ export class TeamRuntime {
|
|
|
520
532
|
// its slowest child and only an Esc ends it early. The lead's idle
|
|
521
533
|
// watchdog is paused for the whole dispatch either way, exactly as it is
|
|
522
534
|
// for the lead's own long turns.
|
|
535
|
+
//
|
|
536
|
+
// D-4, NOW FOR THE WHOLE DISPATCH TOO: with the supervisor gate open the
|
|
537
|
+
// ceiling NEVER arms a hard abort - the cadence ladder is what looks at a
|
|
538
|
+
// long dispatch, exactly as it already looks at a long child. The legacy
|
|
539
|
+
// hard `abortAll()` exists ONLY under `team.overseer: false`, the same
|
|
540
|
+
// regime line `runCtx.subagentTimeoutMs` below draws.
|
|
541
|
+
const dispatchCeilingMs = team.overseer ? 0 : team.dispatchTimeoutMs;
|
|
523
542
|
this.dispatchTimer =
|
|
524
|
-
|
|
525
|
-
? setTimeout(() => this.abortAll(), team.dispatchTimeoutMs)
|
|
526
|
-
: null;
|
|
543
|
+
dispatchCeilingMs > 0 ? setTimeout(() => this.abortAll(), dispatchCeilingMs) : null;
|
|
527
544
|
const runCtx = {
|
|
528
545
|
dispatchId,
|
|
529
546
|
handles,
|
package/dist/team/subagent.d.ts
CHANGED
|
@@ -135,6 +135,16 @@ export interface SubagentHooks {
|
|
|
135
135
|
* so the phase read at fire time is current.
|
|
136
136
|
*/
|
|
137
137
|
onActivity?: () => void;
|
|
138
|
+
/**
|
|
139
|
+
* THE TURN CAP AS AN INSPECTION TRIGGER (F-8 A'2). Called ONCE, at the turn
|
|
140
|
+
* that crossed `maxTurnsPerSubagent`, ONLY when the dispatch supervisor is
|
|
141
|
+
* live - the runtime wires this hook under the same gate as
|
|
142
|
+
* `overseerActive`, so its presence IS the supervised/legacy branch. The
|
|
143
|
+
* child is NOT stopped: the supervisor decides wait / nudge / replace /
|
|
144
|
+
* abandon through the same `inspectAndApply` path every other trigger
|
|
145
|
+
* uses, and a child it lets run carries no `truncated` flag.
|
|
146
|
+
*/
|
|
147
|
+
onTurnCapReached?: (label: string) => void;
|
|
138
148
|
}
|
|
139
149
|
export interface SubagentHandle {
|
|
140
150
|
spec: SubagentSpec;
|
package/dist/team/subagent.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createBuiltinTools } from '../tools/index.js';
|
|
|
18
18
|
import { pickActivityArgs, sanitizeActivity } from './activity.js';
|
|
19
19
|
import { TEAM_LIMITS, TEAM_SUBAGENT_TOOL_NAMES } from './limits.js';
|
|
20
20
|
import { isRetryableStreamError } from './retry.js';
|
|
21
|
+
import { resolveSubagentKey, resolveSubagentModel } from './model.js';
|
|
21
22
|
import { makeTeamSend, makeTeamWait, withMailboxTail } from './comm-tools.js';
|
|
22
23
|
import { buildSubagentBlock } from './prompt.js';
|
|
23
24
|
/** The default factory: a real `Agent` from `@aragon-agent/core`. */
|
|
@@ -162,25 +163,34 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
162
163
|
let agentRef = null;
|
|
163
164
|
const getAgent = () => agentRef;
|
|
164
165
|
const tools = buildSubagentTools(spec, deps, getAgent);
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
166
|
+
// WHICH MODEL A CHILD RUNS IS A USER SETTING, RESOLVED FROM THE
|
|
167
|
+
// DISPATCH-TIME CONFIG through the one authority (subagent-model §3.4).
|
|
168
|
+
// `team.model: 'main'` - the default - is the historical main-agent parity
|
|
169
|
+
// exactly: same provider, model, base URL and thinking level as the lead.
|
|
170
|
+
// `'fast'` / `'profile'` are the user's explicit, visible, reversible cost
|
|
171
|
+
// switches, and EVERY failure mode (tier off, profile deleted, profile key
|
|
172
|
+
// missing) degrades back to the main connection with a disclosed reason
|
|
173
|
+
// rather than failing the dispatch (D-4). The MODEL itself still cannot
|
|
174
|
+
// choose: `normalizeSubagentSpecs` drops a stale `model` key (D-1).
|
|
170
175
|
//
|
|
171
176
|
// `maxTokens` inherits `deps.config.maxTokens` below - the same ambition the
|
|
172
|
-
// lead sends, clamped by
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
// lead sends, clamped by Core's `resolveOutputTokens` against the CHILD
|
|
178
|
+
// model's own ceiling, so an inherited large value degrades to the right
|
|
179
|
+
// number rather than to an HTTP 400.
|
|
180
|
+
const resolution = resolveSubagentModel(deps.config, (id, role) => Boolean(deps.getApiKey(id, role)));
|
|
181
|
+
const model = resolution.ref;
|
|
182
|
+
// THE CHILD'S OWN KEY CLOSURE, PROFILE-AWARE (§3.3 / I-4). A bound profile's
|
|
183
|
+
// dedicated key is visible ONLY inside this closure - never in env, config
|
|
184
|
+
// `apiKeys` or a log line - while every other provider resolves through the
|
|
185
|
+
// lead's resolver at `resolution.keyRole`, which is what keeps a fast child
|
|
186
|
+
// entitled to the fast-role key and a main child to the main one.
|
|
187
|
+
const childGetApiKey = (id) => resolveSubagentKey(deps.config, resolution, id, (providerId, role) => deps.getApiKey(providerId, role));
|
|
188
|
+
// The boolean twin, for the child's compaction summarizer (V-4): the child's
|
|
189
|
+
// own resolution first, the lead layer by role second. Forwarding only
|
|
190
|
+
// `deps.getApiKey` would hide a profile-dedicated key from `resolveSummarizer`
|
|
191
|
+
// and silently disable the child's compaction - the exact gap the req-level
|
|
192
|
+
// resolvers on `ChildContextManagerRequest` exist to close.
|
|
193
|
+
const childHasKey = (id, role) => Boolean(childGetApiKey(id) ?? deps.getApiKey(id, role));
|
|
184
194
|
// HOISTED TO A LOCAL, AND THAT IS WHAT MAKES W3 POSSIBLE AT ALL. This was built
|
|
185
195
|
// INLINE inside the `agentFactory({...})` argument, so there was nothing for the
|
|
186
196
|
// child manager's `getSystemPrompt` accessor to close over - and the accessor
|
|
@@ -216,7 +226,12 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
216
226
|
const contextManager = deps.contextManagerFor?.({
|
|
217
227
|
label: spec.label,
|
|
218
228
|
model,
|
|
219
|
-
|
|
229
|
+
// keyRole (V-4): a fast child's summarizer resolves its main choice with
|
|
230
|
+
// role='fast', so a fast-only key is visible to it; profile keys reach it
|
|
231
|
+
// through the req-level resolvers below instead.
|
|
232
|
+
role: resolution.keyRole,
|
|
233
|
+
getApiKey: childGetApiKey,
|
|
234
|
+
hasKey: childHasKey,
|
|
220
235
|
getMessages: () => agentRef?.state.messages ?? [],
|
|
221
236
|
getSystemPrompt: () => systemPrompt,
|
|
222
237
|
onCompacted: () => {
|
|
@@ -227,12 +242,13 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
227
242
|
systemPrompt,
|
|
228
243
|
model,
|
|
229
244
|
tools,
|
|
230
|
-
thinkingLevel:
|
|
245
|
+
thinkingLevel: resolution.thinkingLevel,
|
|
231
246
|
// The LEAD's registry instance, REUSED (P2-2). It holds adapter state, and
|
|
232
247
|
// constructing five of them per dispatch is pure waste.
|
|
233
248
|
providerRegistry: deps.providerRegistry,
|
|
234
|
-
// The
|
|
235
|
-
|
|
249
|
+
// The child's own profile-aware closure, so a settings-screen key edit
|
|
250
|
+
// reaches children too and a bound profile's key reaches ONLY them.
|
|
251
|
+
getApiKey: childGetApiKey,
|
|
236
252
|
...(deps.config.maxTokens !== undefined ? { maxTokens: deps.config.maxTokens } : {}),
|
|
237
253
|
timeouts: {
|
|
238
254
|
toolTimeout: deps.config.toolTimeoutMs,
|
|
@@ -359,12 +375,26 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
359
375
|
// not runaway - it is working.
|
|
360
376
|
const turnCap = deps.config.team.maxTurnsPerSubagent;
|
|
361
377
|
if (turnCap > 0 && run.turns >= turnCap) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
378
|
+
if (hooks.onTurnCapReached !== undefined) {
|
|
379
|
+
// A'2, D-4 for the turn cap: SUPERVISED means the ceiling is
|
|
380
|
+
// an inspection trigger, not a death sentence. ONE look at the
|
|
381
|
+
// crossing turn (`===`), never a per-turn storm; the child
|
|
382
|
+
// keeps working unless the supervisor decides replace or
|
|
383
|
+
// abandon, and a child that keeps running carries NO
|
|
384
|
+
// `truncated` flag - the report must not call it partial.
|
|
385
|
+
if (run.turns === turnCap)
|
|
386
|
+
hooks.onTurnCapReached(spec.label);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// LEGACY regime (no supervisor for this dispatch): the hard
|
|
390
|
+
// stop, byte for byte what it always was.
|
|
391
|
+
run.truncated = true;
|
|
392
|
+
// THE FLAG FIRST, exactly like `handle.abort()`: the child's
|
|
393
|
+
// reviewer reads `abortRequested` as its guard-1 fact, and a
|
|
394
|
+
// turn-cap stop must be visible to it in the same tick.
|
|
395
|
+
abortRequested = true;
|
|
396
|
+
agent.abort();
|
|
397
|
+
}
|
|
368
398
|
}
|
|
369
399
|
break;
|
|
370
400
|
}
|
package/dist/team/task-tool.d.ts
CHANGED
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
* in the schema would behave differently depending on whether `ajv`, an OPTIONAL
|
|
21
21
|
* dependency of core, happened to install.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* model
|
|
25
|
-
*
|
|
26
|
-
* a delegation is never a downgrade
|
|
27
|
-
*
|
|
23
|
+
* WHICH MODEL A CHILD RUNS IS THE USER'S SETTING, NOT THE MODEL'S (D-1):
|
|
24
|
+
* `team.model` defaults to the lead's own connection (main-agent parity) and
|
|
25
|
+
* can name the fast tier or a profile; there is still no per-child `model`
|
|
26
|
+
* property, so a delegation is never a self-downgrade. Resolution lives in
|
|
27
|
+
* `team/model.ts` and is re-read from the LIVE config at every dispatch; the
|
|
28
|
+
* fast tier otherwise reaches children the way it reaches the lead
|
|
29
|
+
* (compaction summaries, per-child reviews).
|
|
28
30
|
*/
|
|
29
31
|
import { type AgentTool } from '@aragon-agent/core';
|
|
30
32
|
import type { ModelCost } from '@aragon-agent/core';
|
|
33
|
+
import type { SubagentModelResolution } from './model.js';
|
|
31
34
|
import type { TeamRuntime } from './runtime.js';
|
|
32
35
|
export interface TaskToolDeps {
|
|
33
36
|
runtime: TeamRuntime;
|
|
@@ -37,8 +40,24 @@ export interface TaskToolDeps {
|
|
|
37
40
|
/** Whether a usable key exists for the active provider right now (P2-3). */
|
|
38
41
|
hasApiKey: () => boolean;
|
|
39
42
|
activeProvider: () => string;
|
|
40
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The cost table of the model children ACTUALLY run (subagent-model §3.4).
|
|
45
|
+
*
|
|
46
|
+
* The controller resolves `team.model` live and prices `modelCost` at the
|
|
47
|
+
* EFFECTIVE model's table: with the default `main` mode this is exactly the
|
|
48
|
+
* lead's own table, so the report's spend line is byte-identical to the
|
|
49
|
+
* parity behavior; with `fast` / `profile` it is that model's table, and an
|
|
50
|
+
* UNPRICED one yields `undefined` so the header says `pricing unknown`
|
|
51
|
+
* rather than fabricating `$0.00` (C-11 / RV-4).
|
|
52
|
+
*/
|
|
41
53
|
modelCost: () => ModelCost | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* The dispatch-time subagent model resolution, for the report's disclosure
|
|
56
|
+
* line (D-4): rendered ONLY when children did not run the main model, and
|
|
57
|
+
* carrying the fallback reason when they could not. OPTIONAL so existing
|
|
58
|
+
* construction sites compile; absent simply omits the line.
|
|
59
|
+
*/
|
|
60
|
+
subagentModel?: () => SubagentModelResolution;
|
|
42
61
|
/**
|
|
43
62
|
* The FAST tier's cost table, so the report's supervisor spend line states
|
|
44
63
|
* real spend (subagent-overseer-v2 D-7 / R-P1-2). OPTIONAL and returning
|
package/dist/team/task-tool.js
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
* in the schema would behave differently depending on whether `ajv`, an OPTIONAL
|
|
21
21
|
* dependency of core, happened to install.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* model
|
|
25
|
-
*
|
|
26
|
-
* a delegation is never a downgrade
|
|
27
|
-
*
|
|
23
|
+
* WHICH MODEL A CHILD RUNS IS THE USER'S SETTING, NOT THE MODEL'S (D-1):
|
|
24
|
+
* `team.model` defaults to the lead's own connection (main-agent parity) and
|
|
25
|
+
* can name the fast tier or a profile; there is still no per-child `model`
|
|
26
|
+
* property, so a delegation is never a self-downgrade. Resolution lives in
|
|
27
|
+
* `team/model.ts` and is re-read from the LIVE config at every dispatch; the
|
|
28
|
+
* fast tier otherwise reaches children the way it reaches the lead
|
|
29
|
+
* (compaction summaries, per-child reviews).
|
|
28
30
|
*/
|
|
29
31
|
import { errorResult, textResult } from '@aragon-agent/core';
|
|
30
32
|
import { TEAM_LIMITS } from './limits.js';
|
|
@@ -39,8 +41,9 @@ const TEAM_OFF_REFUSAL = 'Team mode is off for this session. Do this work yourse
|
|
|
39
41
|
*/
|
|
40
42
|
const TASK_DESCRIPTION = 'Run several independent subagents in parallel and get one combined report. Each ' +
|
|
41
43
|
'subagent is a fresh agent with the same tools and working directory but NO memory of ' +
|
|
42
|
-
'this conversation - its prompt must be self-contained. Each runs on the
|
|
43
|
-
'
|
|
44
|
+
'this conversation - its prompt must be self-contained. Each runs on the model the ' +
|
|
45
|
+
'user configured for subagents (the main model by default). Use this when a task ' +
|
|
46
|
+
'splits ' +
|
|
44
47
|
'into 2 or more parts that do not depend on each other\'s output, especially reading or ' +
|
|
45
48
|
'searching several areas at once. Do NOT use it when one part needs another part\'s ' +
|
|
46
49
|
'result (do those yourself, in order), when two parts would edit the same file, or for ' +
|
|
@@ -120,13 +123,16 @@ export function createTaskTool(deps) {
|
|
|
120
123
|
// A cancellation or a partial result rendered as a tool FAILURE invites
|
|
121
124
|
// the model to retry the whole fan-out, which is the most expensive
|
|
122
125
|
// possible reaction (the same reasoning as `ask_user`'s cancelled
|
|
123
|
-
// shape, and D-14 one level up).
|
|
124
|
-
//
|
|
126
|
+
// shape, and D-14 one level up). `modelCost` is priced at the model
|
|
127
|
+
// children ACTUALLY ran - the lead's own table under the default
|
|
128
|
+
// `main` mode - so the total stays one honest price.
|
|
125
129
|
const cost = deps.modelCost();
|
|
126
130
|
const fastCost = deps.fastModelCost?.();
|
|
131
|
+
const subagentModel = deps.subagentModel?.();
|
|
127
132
|
return textResult(buildDispatchReport(outcome, {
|
|
128
133
|
...(cost ? { cost } : {}),
|
|
129
134
|
...(fastCost ? { fastCost } : {}),
|
|
135
|
+
...(subagentModel ? { subagentModel } : {}),
|
|
130
136
|
}));
|
|
131
137
|
},
|
|
132
138
|
};
|
package/dist/team/types.d.ts
CHANGED
|
@@ -201,13 +201,14 @@ export interface OverseerIntervention {
|
|
|
201
201
|
action: OverseerAction;
|
|
202
202
|
reason: string;
|
|
203
203
|
/**
|
|
204
|
-
* What the trigger was: event stall, wall-clock check,
|
|
205
|
-
* the supervisor
|
|
204
|
+
* What the trigger was: event stall, wall-clock check, the child's turn
|
|
205
|
+
* cap (F-8 A'2 - an inspection the supervisor then ruled on), post-mortem,
|
|
206
|
+
* or the supervisor going QUIET (subagent-overseer-v2 D-10) - the look
|
|
206
207
|
* budget ran out and supervision stopped waking this child. A VALUE OF
|
|
207
208
|
* THE UNION, never a reason prefix: a human-readable sentence is not a
|
|
208
209
|
* machine contract (the `run.error` rule).
|
|
209
210
|
*/
|
|
210
|
-
trigger: 'silence' | 'clock' | 'postmortem' | 'quiet';
|
|
211
|
+
trigger: 'silence' | 'clock' | 'turncap' | 'postmortem' | 'quiet';
|
|
211
212
|
}
|
|
212
213
|
/**
|
|
213
214
|
* A terminal supervisor decision handed to the WORKER LOOP, which is the only
|
package/dist/ui/App.js
CHANGED
|
@@ -2144,6 +2144,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2144
2144
|
// `auto`, not an empty string: an empty field reads as "unset", which
|
|
2145
2145
|
// is indistinguishable from AUTO and is not a state this can be in.
|
|
2146
2146
|
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
|
|
2147
|
+
// BOTH HALVES, seeded AND read back (§6.3): the sibling comment above
|
|
2148
|
+
// records what omitting either one costs. `auto` mirrors the row's
|
|
2149
|
+
// empty-field reading; a bound Main profile hides the row entirely
|
|
2150
|
+
// and the profile editor seeds from the entry instead.
|
|
2151
|
+
contextWindow: cfg.contextWindow === null ? 'auto' : String(cfg.contextWindow),
|
|
2147
2152
|
apiKey: '',
|
|
2148
2153
|
logLevel: cfg.log.level,
|
|
2149
2154
|
// The four fast rows, seeded through the pure mapper so the screen and
|
|
@@ -9,8 +9,13 @@ import { stripEnterFrames } from '../enter-frames.js';
|
|
|
9
9
|
import { pickGlyphs } from '../glyphs.js';
|
|
10
10
|
import { profileEndpointSummary, useProfileScroll } from './ModelProfilePicker.js';
|
|
11
11
|
import { finishProfileEditor } from './model-profile-state.js';
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// `Context window` sits right after Model: the window is an attribute of the
|
|
13
|
+
// model+endpoint the two rows above name. 14 characters, inside the 18-column
|
|
14
|
+
// label budget `LABELS.map(...padEnd(18))` enforces below.
|
|
15
|
+
const FIELDS = ['name', 'provider', 'model', 'contextWindow', 'baseUrl', 'credentialMode',
|
|
16
|
+
'apiKey'];
|
|
17
|
+
const LABELS = ['Name', 'Provider', 'Model', 'Context window', 'Base URL', 'Credential mode',
|
|
18
|
+
'API key'];
|
|
14
19
|
function useEditorInput(props, navigation) {
|
|
15
20
|
const { editor, dispatch } = props;
|
|
16
21
|
const { index, setIndex, retain, count } = navigation;
|
|
@@ -83,8 +88,11 @@ function buildEditorRows(props, navigation) {
|
|
|
83
88
|
: editor.credentialMode === 'keep' ? 'Configured (unchanged)' : 'Shared / environment';
|
|
84
89
|
if (field === 'credentialMode')
|
|
85
90
|
return editor.credentialMode;
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// `(auto)` EXPLICITLY, or an empty window field would fall through to
|
|
92
|
+
// `(required)` - and AUTO is this field's natural state, never a gap.
|
|
93
|
+
return editor[field] || (field === 'name' ? '(defaults to model)'
|
|
94
|
+
: field === 'contextWindow' ? '(auto)' : field === 'baseUrl'
|
|
95
|
+
? '(provider default)' : '(required)');
|
|
88
96
|
});
|
|
89
97
|
const lines = LABELS.map((label, at) => `${label.padEnd(18)}${values[at]}`);
|
|
90
98
|
if (retain)
|
|
@@ -92,6 +100,12 @@ function buildEditorRows(props, navigation) {
|
|
|
92
100
|
lines.push('Apply to settings draft', 'Cancel editing');
|
|
93
101
|
const rows = lines.map((line, at) => _jsxs(Text, { wrap: "truncate", color: at === index ? props.theme.accent : props.theme.muted, children: [at === index ? `${glyphs.caret} ` : ' ', line] }, at));
|
|
94
102
|
rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: ["Target: ", editor.provider, " ", profileEndpointSummary(editor.baseUrl)] }, "target"));
|
|
103
|
+
// HONESTY FOR THE FAST-ONLY ENTRY (§6.5): a window saved on a profile that
|
|
104
|
+
// only ever serves the Fast role does not take effect today, and saying so
|
|
105
|
+
// beats letting the user believe it does.
|
|
106
|
+
if (props.users === 'Fast') {
|
|
107
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: "Window applies when this profile is bound as Main." }, "window-scope"));
|
|
108
|
+
}
|
|
95
109
|
return rows;
|
|
96
110
|
}
|
|
97
111
|
/** Replacement key input starts empty; retention across targets is explicit. */
|