@cad0p/pi-subagents-tintinweb 0.14.2 → 0.22.0
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 +20 -1
- package/CONTRIBUTING.md +8 -0
- package/README.md +98 -58
- package/docs/fork-differences.md +51 -0
- package/docs/verification-checklist.md +132 -0
- package/examples/agent-tool-description.md +3 -9
- package/package.json +13 -11
- package/pnpm-workspace.yaml +9 -0
- package/src/abortable.ts +43 -0
- package/src/agent-manager.ts +404 -140
- package/src/agent-runner.ts +462 -89
- package/src/agent-types.ts +269 -23
- package/src/checkpoint.ts +49 -0
- package/src/child-context.ts +30 -0
- package/src/cross-extension-rpc.ts +65 -5
- package/src/custom-agents.ts +161 -14
- package/src/default-agents.ts +1 -1
- package/src/index.ts +1014 -980
- package/src/invocation-config.ts +81 -12
- package/src/model-resolver.ts +18 -0
- package/src/model-scope.ts +62 -0
- package/src/nested-tools.ts +406 -0
- package/src/output-file.ts +18 -1
- package/src/prompts.ts +71 -0
- package/src/result-report.ts +170 -0
- package/src/schedule-store.ts +300 -13
- package/src/schedule.ts +222 -77
- package/src/settings.ts +206 -32
- package/src/status-note.ts +18 -1
- package/src/text-safety.ts +69 -0
- package/src/types.ts +101 -58
- package/src/ui/agent-widget.ts +104 -36
- package/src/ui/conversation-viewer.ts +36 -18
- package/src/ui/fleet-list.ts +19 -8
- package/src/ui/schedule-menu.ts +63 -19
- package/src/usage.ts +128 -8
- package/src/worktree.ts +19 -0
- package/src/group-join.ts +0 -141
package/src/settings.ts
CHANGED
|
@@ -5,7 +5,44 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import
|
|
8
|
+
import { IMPLICIT_FALLBACK, NO_FALLBACK } from "./agent-types.js";
|
|
9
|
+
import type { WidgetMode } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What the `fallbackSubagent` row shows and offers.
|
|
13
|
+
*
|
|
14
|
+
* The current value is always included in `values`. pi-tui's cycle handler
|
|
15
|
+
* computes `values.indexOf(currentValue)` and writes `values[0]` when that is
|
|
16
|
+
* -1, so a row whose current value is absent from its own list turns a single
|
|
17
|
+
* Enter into a silent rewrite to whatever happens to sort first — a custom
|
|
18
|
+
* agent, once the defaults are disabled. Including it makes Enter mean "cycle
|
|
19
|
+
* from what is shown", which is what every other row means.
|
|
20
|
+
*
|
|
21
|
+
* `unresolvable` reports an explicitly configured fallback that no longer names
|
|
22
|
+
* an enabled agent — deleted, disabled, or a default with `disableDefaultAgents`
|
|
23
|
+
* on. Dispatch refuses in that state, so the row must say so rather than
|
|
24
|
+
* presenting it as a working target. `general-purpose` is the exception in both
|
|
25
|
+
* directions: it resolves to the permissive hardcoded tier whether it is
|
|
26
|
+
* configured or unset, and dispatch accepts it either way, so an unset fallback
|
|
27
|
+
* is never unresolvable and neither is an explicit `general-purpose`.
|
|
28
|
+
*/
|
|
29
|
+
export function fallbackRow(
|
|
30
|
+
configured: string | undefined,
|
|
31
|
+
available: readonly string[],
|
|
32
|
+
noneValue: string,
|
|
33
|
+
resolvable: (name: string) => boolean,
|
|
34
|
+
): { value: string; values: string[]; unresolvable: boolean } {
|
|
35
|
+
const value = configured ?? IMPLICIT_FALLBACK;
|
|
36
|
+
// `resolvable` is `isFallbackResolvable`, i.e. the resolver's own policy. The
|
|
37
|
+
// uniqueness rule needs the full registry (a disabled agent still claims its
|
|
38
|
+
// name), which `available` alone cannot express — so the caller supplies the
|
|
39
|
+
// verdict rather than this function re-deriving a half-truth.
|
|
40
|
+
// Only a *configured* value can be unresolvable: an unset fallback deliberately
|
|
41
|
+
// targets the permissive hardcoded tier even when that agent is unregistered or
|
|
42
|
+
// disabled, so calling it broken would report a refusal dispatch never performs.
|
|
43
|
+
const unresolvable = configured !== undefined && value.trim().toLowerCase() !== noneValue && !resolvable(value);
|
|
44
|
+
return { value, values: [...new Set([value, ...available, noneValue])], unresolvable };
|
|
45
|
+
}
|
|
9
46
|
|
|
10
47
|
export interface SubagentsSettings {
|
|
11
48
|
maxConcurrent?: number;
|
|
@@ -16,7 +53,6 @@ export interface SubagentsSettings {
|
|
|
16
53
|
*/
|
|
17
54
|
defaultMaxTurns?: number;
|
|
18
55
|
graceTurns?: number;
|
|
19
|
-
defaultJoinMode?: JoinMode;
|
|
20
56
|
/**
|
|
21
57
|
* Master switch for the schedule subagent feature. Defaults to `true`.
|
|
22
58
|
* When `false`: the `Agent` tool's `schedule` param + its guideline are
|
|
@@ -44,12 +80,20 @@ export interface SubagentsSettings {
|
|
|
44
80
|
* against. Defaults to false: subagents may use any model.
|
|
45
81
|
*/
|
|
46
82
|
scopeModels?: boolean;
|
|
47
|
-
/** Result preview rendering mode. Defaults to "markdown". */
|
|
48
|
-
resultPreviewMode?: ResultPreviewMode;
|
|
49
|
-
/** Always show expanded result preview, ignoring pi's expanded flag. Defaults to true. */
|
|
50
|
-
resultPreviewExpanded?: boolean;
|
|
51
83
|
/** Max chars for failure preview before truncation. Defaults to 65536 (64 KiB). */
|
|
52
84
|
failurePreviewMaxChars?: number;
|
|
85
|
+
/**
|
|
86
|
+
* When true, an unreadable or unparseable agent `.md` aborts extension load
|
|
87
|
+
* instead of being skipped with a warning — pi exits, naming the file.
|
|
88
|
+
*
|
|
89
|
+
* Startup only, by design. Mid-session reloads (one per `Agent` call) keep
|
|
90
|
+
* warning: a bad edit at 3pm should not kill the session on the next
|
|
91
|
+
* unrelated spawn, where the failure would look disconnected from its cause.
|
|
92
|
+
* For a checked-in `.pi/agents/`, failing at startup is the point — the
|
|
93
|
+
* alternative is running a *different* agent than the file names.
|
|
94
|
+
* Defaults to false.
|
|
95
|
+
*/
|
|
96
|
+
strictAgentFiles?: boolean;
|
|
53
97
|
/**
|
|
54
98
|
* When true, the three built-in default agents (general-purpose, Explore, Plan)
|
|
55
99
|
* are not registered at startup. User-defined agents from project/global custom
|
|
@@ -76,13 +120,9 @@ export interface SubagentsSettings {
|
|
|
76
120
|
fleetView?: boolean;
|
|
77
121
|
/**
|
|
78
122
|
* Display mode for the persistent above-editor agent widget:
|
|
79
|
-
* - `all`: show every agent
|
|
80
|
-
* - `background`: hide foreground agents — they already render inline as the
|
|
81
|
-
* Agent tool result, so the widget would otherwise double-render them
|
|
82
|
-
* (#118); everything else (background, queued, scheduled, RPC) stays.
|
|
123
|
+
* - `all`: show every agent.
|
|
83
124
|
* - `off`: hide the widget entirely.
|
|
84
|
-
* Defaults to `
|
|
85
|
-
* widget).
|
|
125
|
+
* Defaults to `all`. Pure-UI and applied live (toggling refreshes the widget).
|
|
86
126
|
*/
|
|
87
127
|
widgetMode?: WidgetMode;
|
|
88
128
|
/**
|
|
@@ -92,10 +132,106 @@ export interface SubagentsSettings {
|
|
|
92
132
|
* project (e.g. a repo that shouldn't leave run transcripts on disk for backup
|
|
93
133
|
* or DLP tooling to ingest). A custom agent's `output_transcript` frontmatter
|
|
94
134
|
* overrides this per agent. This governs only the transcript — it does NOT
|
|
95
|
-
* affect the
|
|
96
|
-
* (`isolation: worktree`), or memory files.
|
|
135
|
+
* affect whether the run's pi session file is retained (`persist_session`),
|
|
136
|
+
* worktree commits (`isolation: worktree`), or memory files.
|
|
97
137
|
*/
|
|
98
138
|
outputTranscript?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Hard ceiling on nested subagent delegation, counted from the main session:
|
|
141
|
+
* main = 0, its subagents = 1, their children = 2. Defaults to `2`; `0` or `1`
|
|
142
|
+
* disables nesting project-wide. Read when a subagent session is built, so a
|
|
143
|
+
* change applies to agents started after it. A per-agent `max_subagent_depth`
|
|
144
|
+
* can only tighten this, never raise it.
|
|
145
|
+
*/
|
|
146
|
+
maxSubagentDepth?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Whether `isolation: "worktree"` may create a worktree at all. Defaults to
|
|
149
|
+
* `true`. Set `false` on a repo where worktrees are too slow or too large to
|
|
150
|
+
* be worth it: a requested worktree is then dropped and the agent runs in the
|
|
151
|
+
* main checkout.
|
|
152
|
+
*
|
|
153
|
+
* The drop is deliberately silent — there is no per-result note, because the
|
|
154
|
+
* setting exists for projects whose model asks for a worktree on every call,
|
|
155
|
+
* where a note would be noise on every result. What keeps the orchestrator
|
|
156
|
+
* from claiming a `pi-agent-*` branch anyway is that it is never told the
|
|
157
|
+
* capability exists: `isolationParam` (invocation-config.ts) drops the field
|
|
158
|
+
* from the tool schema, and `isolationGuideline` (index.ts) drops the
|
|
159
|
+
* matching prose from the full and compact descriptions — a custom one opts
|
|
160
|
+
* in via the `{{isolationGuideline}}` placeholder. Anything that
|
|
161
|
+
* reintroduces the prose has to reintroduce a note with it.
|
|
162
|
+
*
|
|
163
|
+
* Deliberately a downgrade rather than an error. The fail-loud rule covers
|
|
164
|
+
* worktrees that *cannot* be created; this is the user declining one, and
|
|
165
|
+
* throwing would reject exactly the calls that the `isolation: "off"` value
|
|
166
|
+
* exists to tolerate. Enforced below the tool boundary, so it also covers the
|
|
167
|
+
* scheduler and the unvalidated cross-extension RPC path.
|
|
168
|
+
*/
|
|
169
|
+
worktreeIsolation?: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Agent type substituted when a caller-supplied `subagent_type` doesn't
|
|
172
|
+
* resolve to exactly one enabled agent (unknown, disabled, or ambiguous by
|
|
173
|
+
* case). Omitted keeps the historical `general-purpose` fallback; a type name
|
|
174
|
+
* routes those calls to that agent instead; `"none"` disables the fallback so
|
|
175
|
+
* dispatch fails closed with an error naming the available types.
|
|
176
|
+
*
|
|
177
|
+
* The boolean `false` is accepted as a spelling of `"none"`, because a boolean
|
|
178
|
+
* would otherwise be dropped as the wrong type and silently leave the
|
|
179
|
+
* PERMISSIVE default in place while the author believes strict dispatch is on
|
|
180
|
+
* — the wrong direction to fail for this setting. Every other value is an
|
|
181
|
+
* agent name, so a mistaken `"off"` fails loudly at dispatch rather than
|
|
182
|
+
* meaning one thing here and another in the resolver.
|
|
183
|
+
*/
|
|
184
|
+
fallbackSubagent?: string;
|
|
185
|
+
/**
|
|
186
|
+
* Whether this extension's tool results carry a `usage` field, so subagent
|
|
187
|
+
* spend reaches the parent session's own accounting. Defaults to `true`.
|
|
188
|
+
*
|
|
189
|
+
* Subagents run in their own pi sessions, so without this the parent's footer,
|
|
190
|
+
* statusline and `/cost` show only what the main model spent — a session that
|
|
191
|
+
* delegated most of its work reads as nearly free. Pi folds
|
|
192
|
+
* `toolResult.usage` into `getSessionStats()`, so attaching it makes those
|
|
193
|
+
* surfaces count subagents too, under `/cost`'s "Tools/summaries" bucket.
|
|
194
|
+
*
|
|
195
|
+
* On by default because the alternative is a total that is quietly wrong about
|
|
196
|
+
* what the session cost; upstream defaults it off, this fork does not. The
|
|
197
|
+
* honest caveat stands: turning it on makes numbers move that a statusline may
|
|
198
|
+
* already have been tracking, which is why it remains a setting.
|
|
199
|
+
*
|
|
200
|
+
* Three properties of what gets reported:
|
|
201
|
+
* - `cacheRead` IS included, because pi counts the parent's own messages
|
|
202
|
+
* that way and the prefix really is re-billed on every call. The display
|
|
203
|
+
* total (`getLifetimeTotal`) keeps excluding it — see `usage.ts`.
|
|
204
|
+
* - Cost is pi's own per-message `usage.cost.total`; we price nothing, and
|
|
205
|
+
* a model pi has no rates for contributes 0.
|
|
206
|
+
* - The context-window percentage is untouched. Pi derives it from assistant
|
|
207
|
+
* messages alone (`getContextUsage`), so a delegating session's context
|
|
208
|
+
* does not appear to fill up faster.
|
|
209
|
+
*/
|
|
210
|
+
reportUsage?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Whether the subagent surfaces show an estimated dollar cost next to their
|
|
213
|
+
* token counts (widget, FleetView, conversation viewer, `get_subagent_result`,
|
|
214
|
+
* completion notifications). Defaults to `true`. Applied live.
|
|
215
|
+
*
|
|
216
|
+
* Rendered as `~$0.0042` — the tilde marks it as pi's reported estimate rather
|
|
217
|
+
* than a billed figure, and it is omitted entirely when the model has no
|
|
218
|
+
* pricing data, so a local model shows tokens and no dollars.
|
|
219
|
+
*
|
|
220
|
+
* Independent of `reportUsage`: this one is what a human reads, that one is
|
|
221
|
+
* what the parent session counts.
|
|
222
|
+
*/
|
|
223
|
+
showCost?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Whether the widget's running rows name the model driving each agent and the
|
|
226
|
+
* thinking level it is running at. Defaults to `false`.
|
|
227
|
+
*
|
|
228
|
+
* Off by default because the widget row already carries the description,
|
|
229
|
+
* turns, tool uses, tokens and elapsed time, and every character it gains is
|
|
230
|
+
* one the description loses on a narrow terminal. The conversation viewer and
|
|
231
|
+
* the `get_subagent_result` header show the pair either way — they have a line
|
|
232
|
+
* to themselves.
|
|
233
|
+
*/
|
|
234
|
+
showModel?: boolean;
|
|
99
235
|
}
|
|
100
236
|
|
|
101
237
|
export type ToolDescriptionMode = "full" | "compact" | "custom";
|
|
@@ -105,26 +241,28 @@ export interface SettingsAppliers {
|
|
|
105
241
|
setMaxConcurrent: (n: number) => void;
|
|
106
242
|
setDefaultMaxTurns: (n: number) => void;
|
|
107
243
|
setGraceTurns: (n: number) => void;
|
|
108
|
-
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
109
244
|
setSchedulingEnabled: (b: boolean) => void;
|
|
110
245
|
setScopeModels: (enabled: boolean) => void;
|
|
111
|
-
|
|
112
|
-
setResultPreviewExpanded: (expanded: boolean) => void;
|
|
246
|
+
setStrictAgentFiles: (b: boolean) => void;
|
|
113
247
|
setFailurePreviewMaxChars: (chars: number) => void;
|
|
114
248
|
setDisableDefaultAgents: (b: boolean) => void;
|
|
115
249
|
setToolDescriptionMode: (mode: ToolDescriptionMode) => void;
|
|
116
250
|
setFleetView: (b: boolean) => void;
|
|
117
251
|
setWidgetMode: (mode: WidgetMode) => void;
|
|
118
252
|
setOutputTranscript: (b: boolean) => void;
|
|
253
|
+
setMaxSubagentDepth: (n: number) => void;
|
|
254
|
+
setWorktreeIsolation: (b: boolean) => void;
|
|
255
|
+
setFallbackSubagent: (v: string | undefined) => void;
|
|
256
|
+
setReportUsage: (b: boolean) => void;
|
|
257
|
+
setShowCost: (b: boolean) => void;
|
|
258
|
+
setShowModel: (b: boolean) => void;
|
|
119
259
|
}
|
|
120
260
|
|
|
121
261
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
122
262
|
export type SettingsEmit = (event: string, payload: unknown) => void;
|
|
123
263
|
|
|
124
|
-
const VALID_JOIN_MODES: ReadonlySet<string> = new Set<JoinMode>(["async", "group", "smart"]);
|
|
125
|
-
const VALID_RESULT_PREVIEW_MODES: ReadonlySet<string> = new Set<ResultPreviewMode>(["plain", "markdown"]);
|
|
126
264
|
const VALID_TOOL_DESCRIPTION_MODES: ReadonlySet<string> = new Set<ToolDescriptionMode>(["full", "compact", "custom"]);
|
|
127
|
-
const VALID_WIDGET_MODES: ReadonlySet<string> = new Set<WidgetMode>(["all", "
|
|
265
|
+
const VALID_WIDGET_MODES: ReadonlySet<string> = new Set<WidgetMode>(["all", "off"]);
|
|
128
266
|
|
|
129
267
|
|
|
130
268
|
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
@@ -133,7 +271,16 @@ const VALID_WIDGET_MODES: ReadonlySet<string> = new Set<WidgetMode>(["all", "bac
|
|
|
133
271
|
const MAX_CONCURRENT_CEILING = 1024;
|
|
134
272
|
const MAX_TURNS_CEILING = 10_000;
|
|
135
273
|
const GRACE_TURNS_CEILING = 1_000;
|
|
136
|
-
|
|
274
|
+
const SUBAGENT_DEPTH_CEILING = 16;
|
|
275
|
+
export const FAILURE_PREVIEW_MAX_CHARS_CEILING = 1_048_576;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Shipped default for `showCost`. Named because the report renderer keeps its
|
|
279
|
+
* own copy of the flag (see `result-report.ts`): a file that omits the key
|
|
280
|
+
* applies nothing, so both copies must start from the same value or the widget
|
|
281
|
+
* and the result report disagree about showing a cost.
|
|
282
|
+
*/
|
|
283
|
+
export const DEFAULT_SHOW_COST = true;
|
|
137
284
|
|
|
138
285
|
/** Drop fields that don't match the expected shape. Silent — garbage becomes absent. */
|
|
139
286
|
function sanitize(raw: unknown): SubagentsSettings {
|
|
@@ -161,8 +308,12 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
161
308
|
) {
|
|
162
309
|
out.graceTurns = r.graceTurns as number;
|
|
163
310
|
}
|
|
164
|
-
if (
|
|
165
|
-
|
|
311
|
+
if (
|
|
312
|
+
Number.isInteger(r.maxSubagentDepth) &&
|
|
313
|
+
(r.maxSubagentDepth as number) >= 0 &&
|
|
314
|
+
(r.maxSubagentDepth as number) <= SUBAGENT_DEPTH_CEILING
|
|
315
|
+
) {
|
|
316
|
+
out.maxSubagentDepth = r.maxSubagentDepth as number;
|
|
166
317
|
}
|
|
167
318
|
if (typeof r.schedulingEnabled === "boolean") {
|
|
168
319
|
out.schedulingEnabled = r.schedulingEnabled;
|
|
@@ -170,16 +321,13 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
170
321
|
if (typeof r.scopeModels === "boolean") {
|
|
171
322
|
out.scopeModels = r.scopeModels;
|
|
172
323
|
}
|
|
173
|
-
if (typeof r.
|
|
174
|
-
out.
|
|
175
|
-
}
|
|
176
|
-
if (typeof r.resultPreviewExpanded === "boolean") {
|
|
177
|
-
out.resultPreviewExpanded = r.resultPreviewExpanded;
|
|
324
|
+
if (typeof r.strictAgentFiles === "boolean") {
|
|
325
|
+
out.strictAgentFiles = r.strictAgentFiles;
|
|
178
326
|
}
|
|
179
327
|
if (
|
|
180
328
|
Number.isInteger(r.failurePreviewMaxChars) &&
|
|
181
329
|
(r.failurePreviewMaxChars as number) >= 1 &&
|
|
182
|
-
(r.failurePreviewMaxChars as number) <=
|
|
330
|
+
(r.failurePreviewMaxChars as number) <= FAILURE_PREVIEW_MAX_CHARS_CEILING
|
|
183
331
|
) {
|
|
184
332
|
out.failurePreviewMaxChars = r.failurePreviewMaxChars as number;
|
|
185
333
|
}
|
|
@@ -198,6 +346,28 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
198
346
|
if (typeof r.outputTranscript === "boolean") {
|
|
199
347
|
out.outputTranscript = r.outputTranscript;
|
|
200
348
|
}
|
|
349
|
+
if (typeof r.worktreeIsolation === "boolean") {
|
|
350
|
+
out.worktreeIsolation = r.worktreeIsolation;
|
|
351
|
+
}
|
|
352
|
+
if (typeof r.reportUsage === "boolean") {
|
|
353
|
+
out.reportUsage = r.reportUsage;
|
|
354
|
+
}
|
|
355
|
+
if (typeof r.showCost === "boolean") {
|
|
356
|
+
out.showCost = r.showCost;
|
|
357
|
+
}
|
|
358
|
+
if (typeof r.showModel === "boolean") {
|
|
359
|
+
out.showModel = r.showModel;
|
|
360
|
+
}
|
|
361
|
+
if (r.fallbackSubagent === false) {
|
|
362
|
+
// The only non-string spelling worth accepting: a boolean would otherwise be
|
|
363
|
+
// dropped, silently leaving the PERMISSIVE default in place. Every string is
|
|
364
|
+
// an agent name except the `none` sentinel, which the resolver recognizes —
|
|
365
|
+
// so a mistaken "off" fails loudly at dispatch instead of meaning something
|
|
366
|
+
// different here than it does there.
|
|
367
|
+
out.fallbackSubagent = NO_FALLBACK;
|
|
368
|
+
} else if (typeof r.fallbackSubagent === "string" && r.fallbackSubagent.trim()) {
|
|
369
|
+
out.fallbackSubagent = r.fallbackSubagent.trim();
|
|
370
|
+
}
|
|
201
371
|
return out;
|
|
202
372
|
}
|
|
203
373
|
|
|
@@ -251,17 +421,21 @@ export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers):
|
|
|
251
421
|
if (typeof s.maxConcurrent === "number") appliers.setMaxConcurrent(s.maxConcurrent);
|
|
252
422
|
if (typeof s.defaultMaxTurns === "number") appliers.setDefaultMaxTurns(s.defaultMaxTurns);
|
|
253
423
|
if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns);
|
|
254
|
-
if (s.
|
|
424
|
+
if (typeof s.maxSubagentDepth === "number") appliers.setMaxSubagentDepth(s.maxSubagentDepth);
|
|
255
425
|
if (typeof s.schedulingEnabled === "boolean") appliers.setSchedulingEnabled(s.schedulingEnabled);
|
|
256
426
|
if (typeof s.scopeModels === "boolean") appliers.setScopeModels(s.scopeModels);
|
|
257
|
-
if (s.
|
|
258
|
-
if (typeof s.resultPreviewExpanded === "boolean") appliers.setResultPreviewExpanded(s.resultPreviewExpanded);
|
|
427
|
+
if (typeof s.strictAgentFiles === "boolean") appliers.setStrictAgentFiles(s.strictAgentFiles);
|
|
259
428
|
if (typeof s.failurePreviewMaxChars === "number") appliers.setFailurePreviewMaxChars(s.failurePreviewMaxChars);
|
|
260
429
|
if (typeof s.disableDefaultAgents === "boolean") appliers.setDisableDefaultAgents(s.disableDefaultAgents);
|
|
261
430
|
if (s.toolDescriptionMode) appliers.setToolDescriptionMode(s.toolDescriptionMode);
|
|
262
431
|
if (typeof s.fleetView === "boolean") appliers.setFleetView(s.fleetView);
|
|
263
432
|
if (s.widgetMode) appliers.setWidgetMode(s.widgetMode);
|
|
264
433
|
if (typeof s.outputTranscript === "boolean") appliers.setOutputTranscript(s.outputTranscript);
|
|
434
|
+
if (typeof s.worktreeIsolation === "boolean") appliers.setWorktreeIsolation(s.worktreeIsolation);
|
|
435
|
+
if (typeof s.fallbackSubagent === "string") appliers.setFallbackSubagent(s.fallbackSubagent);
|
|
436
|
+
if (typeof s.reportUsage === "boolean") appliers.setReportUsage(s.reportUsage);
|
|
437
|
+
if (typeof s.showCost === "boolean") appliers.setShowCost(s.showCost);
|
|
438
|
+
if (typeof s.showModel === "boolean") appliers.setShowModel(s.showModel);
|
|
265
439
|
}
|
|
266
440
|
|
|
267
441
|
/**
|
package/src/status-note.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* status-note.ts —
|
|
2
|
+
* status-note.ts — Honest framing for an agent result: the parenthetical status
|
|
3
|
+
* note for a non-normal outcome, and the salvaged partial output of a failure.
|
|
4
|
+
*
|
|
5
|
+
* Lives here rather than in an index.ts closure because both entry points need
|
|
6
|
+
* it — the top-level tools and the nested delegation tools, which cannot import
|
|
7
|
+
* the extension entry point.
|
|
3
8
|
*/
|
|
4
9
|
|
|
10
|
+
import type { AgentRecord } from "./types.js";
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Explicit parenthetical note for a non-normal terminal outcome, so the parent
|
|
7
14
|
* agent can't mistake partial output for a completed result. Empty string for a
|
|
@@ -23,3 +30,13 @@ export function getStatusNote(status: string): string {
|
|
|
23
30
|
return "";
|
|
24
31
|
}
|
|
25
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Salvaged partial output of a failed run, as a labeled suffix for the error
|
|
36
|
+
* surfaces (or "" if the run produced nothing). `record.result` is bounded to
|
|
37
|
+
* the run's own turns, so this is never a stale earlier answer (#144).
|
|
38
|
+
*/
|
|
39
|
+
export function partialOutputSuffix(record: AgentRecord): string {
|
|
40
|
+
const partial = record.result?.trim();
|
|
41
|
+
return partial ? `\n\nPartial output before the failure:\n${partial}` : "";
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* text-safety.ts — shared sanitizer for untrusted text that reaches the
|
|
3
|
+
* terminal or the model context (child output, tool arguments, settings).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Strip terminal control sequences and invisible/forging characters, keeping
|
|
8
|
+
* tab, LF, and printable non-ASCII. Complete OSC and CSI sequences are consumed
|
|
9
|
+
* whole, including the 8-bit C1 form of CSI (U+009B with no `[`); a dangling
|
|
10
|
+
* introducer goes with its ESC/C1 byte, while an unterminated payload (an
|
|
11
|
+
* OSC/DCS sequence missing its terminator) may leave that payload as visible
|
|
12
|
+
* text. CR is dropped, so CRLF collapses to LF and a
|
|
13
|
+
* lone CR cannot overwrite the rendered line. A guard against terminal control
|
|
14
|
+
* and invisible text, not a content filter: markdown and XML-ish characters
|
|
15
|
+
* pass through untouched. The result is always well-formed UTF-16: an
|
|
16
|
+
* unpaired surrogate becomes U+FFFD, or the paired character when a removal
|
|
17
|
+
* above brings a high and a low surrogate together. Leaving one in place
|
|
18
|
+
* would let a padded or clamped row overflow — a terminal renders it as
|
|
19
|
+
* U+FFFD one column wide, while width helpers count it as zero columns.
|
|
20
|
+
*
|
|
21
|
+
* The invisible/format set includes the Unicode Tag block (U+E0001,
|
|
22
|
+
* U+E0020–U+E007F), which encodes invisible ASCII payloads, plus CGJ (U+034F),
|
|
23
|
+
* the Hangul fillers (U+115F/U+1160/U+3164/U+FFA0), the variation-selector
|
|
24
|
+
* supplement (U+E0100–U+E01EF), the zero-width/directional mark range
|
|
25
|
+
* (U+200B–U+200F, including ZWJ U+200D), the line/paragraph separators
|
|
26
|
+
* (U+2028/U+2029), the bidi embedding/override controls (U+202A–U+202E), the
|
|
27
|
+
* U+2060–U+2069 block (word joiner, invisible operators, bidi isolates), soft
|
|
28
|
+
* hyphen (U+00AD), ALM (U+061C), the Mongolian vowel separator (U+180E), and
|
|
29
|
+
* BOM/ZWNBSP (U+FEFF). Stripping those families is a deliberate over-strip for
|
|
30
|
+
* Trojan-Source/covert-channel defense even though they have legitimate roles —
|
|
31
|
+
* CGJ blocks Arabic/Indic ligatures, the VS supplement selects CJK ideographic
|
|
32
|
+
* glyph variants, the Hangul fillers appear in old-Hangul text, ZWJ joins emoji
|
|
33
|
+
* and Indic sequences, and the bidi controls/isolates steer RTL layout.
|
|
34
|
+
* U+FE0E/U+FE0F are deliberately kept: they carry emoji vs text presentation.
|
|
35
|
+
*/
|
|
36
|
+
export function stripControlChars(s: string): string {
|
|
37
|
+
return s
|
|
38
|
+
.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "") // complete OSC: ESC ] … BEL | ST
|
|
39
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "") // complete CSI: ESC [ … final byte
|
|
40
|
+
.replace(/\u009b[0-?]*[ -/]*[@-~]/g, "") // complete C1 CSI: U+009B … final byte
|
|
41
|
+
.replace(/[\u001b\u009b][[\]()#;?]*/g, "") // dangling ESC/C1 plus its introducer
|
|
42
|
+
.replace(
|
|
43
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
|
|
44
|
+
"\uFFFD",
|
|
45
|
+
) // coerce unpaired surrogates
|
|
46
|
+
.replace(
|
|
47
|
+
/[\u{e0100}-\u{e01ef}\u034f\x00-\x08\x0b-\x0d\x0e-\x1f\x7f-\x9f\u00ad\u061c\u115f\u1160\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\u3164\ufeff\uffa0\u{e0001}\u{e0020}-\u{e007f}]/gu,
|
|
48
|
+
"",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Collapse newlines/CRs/tabs to spaces and strip control/invisible characters,
|
|
54
|
+
* for untrusted text composed into a single terminal line (report headers and
|
|
55
|
+
* metadata, tool-call lines, widget rows). Use `stripControlChars` for body
|
|
56
|
+
* and display paths that preserve layout. A display sanitizer, not a type
|
|
57
|
+
* guard: non-string input renders as the empty string.
|
|
58
|
+
*/
|
|
59
|
+
export function toSingleLine(s: unknown): string {
|
|
60
|
+
if (typeof s !== "string") return "";
|
|
61
|
+
return stripControlChars(s.replace(/\r\n?|\n/g, " ").replace(/\t/g, " ")).trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Truncate to maxChars UTF-16 code units, never splitting a surrogate pair. */
|
|
65
|
+
export function safeTruncate(s: string, maxChars: number): string {
|
|
66
|
+
if (s.length <= maxChars) return s;
|
|
67
|
+
const high = s.charCodeAt(maxChars - 1);
|
|
68
|
+
return high >= 0xD800 && high <= 0xDBFF ? s.slice(0, maxChars - 1) : s.slice(0, maxChars);
|
|
69
|
+
}
|