@gavin-hu/my-pi-agent 0.11.4 → 0.13.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/README.md +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -236,7 +236,13 @@ step's `Output`.
|
|
|
236
236
|
they are not written to the parent session.
|
|
237
237
|
- **No cost attribution to the parent session.** A subagent's tokens and cost are
|
|
238
238
|
captured in the tool result and shown in the transcript, but they belong to a
|
|
239
|
-
separate process and are not added to the dispatching session's totals.
|
|
239
|
+
separate process and are not added to the dispatching session's totals. Instead,
|
|
240
|
+
each finished run publishes `my-pi-agent/subagent-finished`
|
|
241
|
+
(`SubagentFinishedEvent` in `lib/events.ts`) and bytefly's footer shows the
|
|
242
|
+
session's child runs as their own `↳` roll-up, priced separately from the
|
|
243
|
+
session total (the last 500 runs). The roll-up is per process: a resumed session
|
|
244
|
+
starts from zero, and a run that never produced an assistant message publishes
|
|
245
|
+
nothing.
|
|
240
246
|
|
|
241
247
|
## Pi integration
|
|
242
248
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The accounting event a finished child run publishes.
|
|
3
|
+
*
|
|
4
|
+
* `subagent` runs each child in its own `pi` process, so the dispatcher's own
|
|
5
|
+
* usage totals never include what the child spent. The event carries the child's
|
|
6
|
+
* pricing inputs (provider, model, token counts, Pi's recorded dollar cost, finish
|
|
7
|
+
* time) rather than a total, so the consumer can price them with the same rate
|
|
8
|
+
* table and currency it uses for the session — and re-price them after a currency
|
|
9
|
+
* change.
|
|
10
|
+
*
|
|
11
|
+
* A run with no assistant message spent nothing observable (an unknown agent, a
|
|
12
|
+
* spawn failure), so it publishes nothing at all.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SubagentFinishedEvent } from "../../lib/events.ts";
|
|
16
|
+
import type { SingleResult } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
/** The event for one finished run, or `null` when it produced no usage. */
|
|
19
|
+
export function finishedEvent(result: SingleResult): SubagentFinishedEvent | null {
|
|
20
|
+
const { usage } = result;
|
|
21
|
+
if (usage.turns === 0) return null;
|
|
22
|
+
return {
|
|
23
|
+
agent: result.agent,
|
|
24
|
+
provider: result.provider ?? null,
|
|
25
|
+
model: result.model ?? null,
|
|
26
|
+
input: usage.input,
|
|
27
|
+
output: usage.output,
|
|
28
|
+
cacheRead: usage.cacheRead,
|
|
29
|
+
cacheWrite: usage.cacheWrite,
|
|
30
|
+
recordedCost: usage.cost,
|
|
31
|
+
at: result.finishedAt ?? null,
|
|
32
|
+
exitCode: result.exitCode,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
} from "./agents.ts";
|
|
28
28
|
import { runChainMode, runParallelMode, runSingleMode, type ModeContext } from "./orchestrate.ts";
|
|
29
29
|
import { renderSubagentCall, renderSubagentResult } from "./render.ts";
|
|
30
|
+
import { SUBAGENT_FINISHED_CHANNEL } from "../../lib/events.ts";
|
|
31
|
+
import { finishedEvent } from "./accounting.ts";
|
|
30
32
|
import { runSingleAgent, type RunOptions } from "./run.ts";
|
|
31
33
|
import { SubagentParams, resolveMode, type SubagentArgs } from "./schema.ts";
|
|
32
34
|
import type { DispatchDefaults, SingleResult, SubagentDetails } from "./types.ts";
|
|
@@ -65,7 +67,27 @@ function requestedAgentNames(args: SubagentArgs): string[] {
|
|
|
65
67
|
|
|
66
68
|
export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): void {
|
|
67
69
|
if (!isExtensionEnabled("subagent")) return;
|
|
68
|
-
const
|
|
70
|
+
const runner = deps.run ?? runSingleAgent;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Account for each finished child, then hand the result on unchanged.
|
|
74
|
+
*
|
|
75
|
+
* Every mode (`single`, `parallel` per task, `chain` per step) calls this once
|
|
76
|
+
* per child, so one wrapper reports each run exactly once. The emit is guarded:
|
|
77
|
+
* a bus listener that throws must not fail a delegation that already ran.
|
|
78
|
+
*/
|
|
79
|
+
const run = async (options: RunOptions): Promise<SingleResult> => {
|
|
80
|
+
const result = await runner(options);
|
|
81
|
+
const event = finishedEvent(result);
|
|
82
|
+
if (event) {
|
|
83
|
+
try {
|
|
84
|
+
pi.events.emit(SUBAGENT_FINISHED_CHANNEL, event);
|
|
85
|
+
} catch {
|
|
86
|
+
// Accounting is best-effort; the run's own result is what matters.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
69
91
|
|
|
70
92
|
pi.registerTool({
|
|
71
93
|
name: TOOL_NAME,
|
|
@@ -74,8 +96,6 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
74
96
|
promptSnippet: "Delegate a task to a specialized subagent with an isolated context window.",
|
|
75
97
|
promptGuidelines: GUIDELINES,
|
|
76
98
|
parameters: SubagentParams,
|
|
77
|
-
exposure: "direct",
|
|
78
|
-
defaultActive: true,
|
|
79
99
|
// Deliberately no `executionMode`: sibling subagent calls are independent
|
|
80
100
|
// (each spawns its own child process), so they stay parallel by default.
|
|
81
101
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
@@ -47,6 +47,7 @@ export function applyEvent(result: SingleResult, event: unknown): boolean {
|
|
|
47
47
|
|
|
48
48
|
if (message.role === "assistant") {
|
|
49
49
|
const assistant = message as Message & {
|
|
50
|
+
provider?: string;
|
|
50
51
|
model?: string;
|
|
51
52
|
stopReason?: string;
|
|
52
53
|
errorMessage?: string;
|
|
@@ -69,6 +70,7 @@ export function applyEvent(result: SingleResult, event: unknown): boolean {
|
|
|
69
70
|
result.usage.cost += usage.cost?.total ?? 0;
|
|
70
71
|
result.usage.contextTokens = usage.totalTokens ?? result.usage.contextTokens;
|
|
71
72
|
}
|
|
73
|
+
if (!result.provider && assistant.provider) result.provider = assistant.provider;
|
|
72
74
|
if (!result.model && assistant.model) result.model = assistant.model;
|
|
73
75
|
if (assistant.stopReason) result.stopReason = assistant.stopReason;
|
|
74
76
|
if (assistant.errorMessage) result.errorMessage = assistant.errorMessage;
|
|
@@ -37,6 +37,8 @@ export interface SingleResult {
|
|
|
37
37
|
messages: Message[];
|
|
38
38
|
stderr: string;
|
|
39
39
|
usage: UsageStats;
|
|
40
|
+
/** Provider and model the child reported; published for the accounting event. */
|
|
41
|
+
provider?: string;
|
|
40
42
|
model?: string;
|
|
41
43
|
stopReason?: string;
|
|
42
44
|
errorMessage?: string;
|
|
@@ -41,9 +41,13 @@ How the `todo` call and its result look in the transcript — the call line as a
|
|
|
41
41
|
mutating tool call (`write`/`edit`/`bash`/`powershell`, or a tool declaring
|
|
42
42
|
`readOnlyHint: false`) without a todo update, it injects one hidden reminder
|
|
43
43
|
and forces a single continuation. A failed or blocked call changed nothing, so
|
|
44
|
-
it never triggers one. It
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
it never triggers one. It fires while the list has unfinished items, and —
|
|
45
|
+
when no list exists at all — only after three such calls in one turn, at least
|
|
46
|
+
one of them a file write (`write`/`edit`, or a tool declaring
|
|
47
|
+
`readOnlyHint: false`), so repeated `bash` inspection is not mistaken for
|
|
48
|
+
unplanned work. A list the model wrote or cleared during the turn is left
|
|
49
|
+
alone. At most one reminder per user turn, and it is dropped on the next user
|
|
50
|
+
turn, so a reader can never trigger it.
|
|
47
51
|
- Declares an `outputSchema` and returns matching `structuredContent`
|
|
48
52
|
(`{ todos, action, error? }`), so codemode/scripts can read the list as data.
|
|
49
53
|
|
|
@@ -157,7 +161,7 @@ depends on config.
|
|
|
157
161
|
|
|
158
162
|
| Integration point | Value |
|
|
159
163
|
|---|---|
|
|
160
|
-
| Tool | `todo`;
|
|
164
|
+
| Tool | `todo`; direct exposure (the default), `executionMode: "sequential"` |
|
|
161
165
|
| Annotations | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` |
|
|
162
166
|
| Output | `outputSchema` `TodoResult` — `{ todos, action, error? }` — also returned as `structuredContent` |
|
|
163
167
|
| Command | `/todos` |
|
|
@@ -196,12 +200,16 @@ depends on config.
|
|
|
196
200
|
[`lib/rails.ts`](../../lib/rails.ts) and [`goal`](../goal/).
|
|
197
201
|
- **The reminder is a bounded nudge, not a nag.** A prompt guideline cannot
|
|
198
202
|
guarantee a timely update, so the extension backs it with one forced
|
|
199
|
-
continuation. `tool_execution_end`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
continuation. `tool_execution_end` counts a successful call by an explicit
|
|
204
|
+
mutator list plus `readOnlyHint: false` — a failed or blocked call is ignored,
|
|
205
|
+
and a false positive costs a whole extra model request, so an unrecognized
|
|
206
|
+
reader is treated as harmless. An open item makes one call enough; a run with
|
|
207
|
+
no list at all must reach `EMPTY_LIST_MUTATIONS` (three) first and include a
|
|
208
|
+
file write, because a shell call cannot be told apart from `rg` and counts as
|
|
209
|
+
work but not as a write; a list the model wrote or cleared this turn is its own
|
|
210
|
+
answer and never reopens that path. `nudged` caps it at one per user turn,
|
|
211
|
+
`setTodos` clears the counters, and `context` drops the hidden message on the
|
|
212
|
+
next user turn so it never accumulates.
|
|
205
213
|
- **Content is sanitized at the boundary.** `normalizeTodos` replaces control
|
|
206
214
|
characters and collapses whitespace before the length and duplicate checks,
|
|
207
215
|
keeping `format.ts` free of terminal concerns and every surface to one row
|
|
@@ -228,7 +236,7 @@ depends on config.
|
|
|
228
236
|
| `types.ts` | `Todo`, `TodoStatus`, `TodoDetails`. |
|
|
229
237
|
| `schema.ts` | TypeBox parameters and pure validation/normalization. |
|
|
230
238
|
| `state.ts` | Branch reconstruction and status queries (pure). |
|
|
231
|
-
| `nudge.ts` | Lag detection: mutating-
|
|
239
|
+
| `nudge.ts` | Lag detection: mutating and file-write classification, due check, reminder text (pure). |
|
|
232
240
|
| `format.ts` | Model-facing and transcript text (pure). |
|
|
233
241
|
| `tui.ts` | Widget and scrollable `/todos` components, drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
234
242
|
| `config.ts` | Widget config load/validation (the `todo` section of `bytefly.json`). |
|
package/extensions/todo/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
16
16
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
17
17
|
import { registerCommands } from "./commands.ts";
|
|
18
18
|
import { loadTodoConfig } from "./config.ts";
|
|
19
|
-
import { formatNudge, isMutatingTool } from "./nudge.ts";
|
|
19
|
+
import { formatNudge, isFileWrite, isMutatingTool } from "./nudge.ts";
|
|
20
20
|
import { createTodoRuntime } from "./runtime.ts";
|
|
21
21
|
import { registerTools } from "./tools.ts";
|
|
22
22
|
|
|
@@ -55,11 +55,13 @@ export default function todo(pi: ExtensionAPI) {
|
|
|
55
55
|
|
|
56
56
|
// Only work that can change the working tree sets the lag flag; read-only
|
|
57
57
|
// tools and the todo call itself are ignored. A failed or blocked call changed
|
|
58
|
-
// nothing, so it must not force a reminder.
|
|
58
|
+
// nothing, so it must not force a reminder. A shell call counts as work but
|
|
59
|
+
// not as a write: its name cannot tell `rg` from `git commit`.
|
|
59
60
|
pi.on("tool_execution_end", (event) => {
|
|
60
61
|
if (event.isError) return;
|
|
61
62
|
const annotations = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;
|
|
62
|
-
if (isMutatingTool(event.toolName, annotations))
|
|
63
|
+
if (!isMutatingTool(event.toolName, annotations)) return;
|
|
64
|
+
runtime.noteWork(isFileWrite(event.toolName, annotations));
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
// When the run is about to settle with unfinished work done since the last
|
package/extensions/todo/nudge.ts
CHANGED
|
@@ -18,12 +18,15 @@ import { TODO_TOOL } from "../../lib/tool-names.ts";
|
|
|
18
18
|
import { currentTodo, hasOpenTodos } from "./state.ts";
|
|
19
19
|
import type { Todo } from "./types.ts";
|
|
20
20
|
|
|
21
|
+
/** Tools that run a command, where reading and writing look the same by name. */
|
|
22
|
+
const SHELL_TOOL_NAMES: ReadonlySet<string> = new Set(["bash", "powershell"]);
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* Tools whose calls mean the working tree may have changed: the built-in
|
|
23
|
-
* writers and shells. Kept explicit so a reader or an MCP tool that does not
|
|
26
|
+
* writers and the shells. Kept explicit so a reader or an MCP tool that does not
|
|
24
27
|
* declare a hint cannot force a continuation.
|
|
25
28
|
*/
|
|
26
|
-
export const MUTATING_TOOL_NAMES: ReadonlySet<string> = new Set(["write", "edit",
|
|
29
|
+
export const MUTATING_TOOL_NAMES: ReadonlySet<string> = new Set(["write", "edit", ...SHELL_TOOL_NAMES]);
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Whether a finished tool call counts as work for lag detection. The `todo`
|
|
@@ -37,22 +40,68 @@ export function isMutatingTool(toolName: string, annotations?: ToolAnnotations):
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
|
-
* Whether a
|
|
41
|
-
*
|
|
43
|
+
* Whether a finished tool call can have changed files, as opposed to a shell
|
|
44
|
+
* command that may only have read them. The reminder for a run that never
|
|
45
|
+
* started a list needs this: three `rg` calls are investigation, not unplanned
|
|
46
|
+
* work, and a shell call cannot be told apart from one that wrote. So it is a
|
|
47
|
+
* mutating call that is not a shell.
|
|
48
|
+
*/
|
|
49
|
+
export function isFileWrite(toolName: string, annotations?: ToolAnnotations): boolean {
|
|
50
|
+
if (toolName === TODO_TOOL || SHELL_TOOL_NAMES.has(toolName)) return false;
|
|
51
|
+
return isMutatingTool(toolName, annotations);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Successful mutating calls needed before a run with no list at all is worth a
|
|
56
|
+
* reminder. A plan that was never started has no item to fall behind, and a
|
|
57
|
+
* false positive costs a whole extra model request, so one edit is not enough
|
|
58
|
+
* evidence that the work needed a list.
|
|
59
|
+
*/
|
|
60
|
+
export const EMPTY_LIST_MUTATIONS = 3;
|
|
61
|
+
|
|
62
|
+
/** What the current turn knows about its work and the list. */
|
|
63
|
+
export interface NudgeState {
|
|
64
|
+
/** Successful mutating calls since the turn began or the list was last written. */
|
|
65
|
+
mutations: number;
|
|
66
|
+
/** How many of those can change files, as opposed to a shell command that may have read. */
|
|
67
|
+
writes: number;
|
|
68
|
+
/** Whether the model wrote or cleared the list since the turn began. */
|
|
69
|
+
touched: boolean;
|
|
70
|
+
/** Whether a reminder was already sent this turn. */
|
|
71
|
+
nudged: boolean;
|
|
72
|
+
/** The current list. */
|
|
73
|
+
todos: Todo[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a reminder is due. With an unfinished item, one mutation is enough.
|
|
78
|
+
* With no list at all, the turn must also have written something: repeated
|
|
79
|
+
* inspection with no file change is not unplanned work. A list the model wrote
|
|
80
|
+
* or cleared this turn is its own answer, so an explicit clear is never
|
|
81
|
+
* second-guessed.
|
|
42
82
|
*/
|
|
43
|
-
export function shouldNudge(
|
|
44
|
-
|
|
83
|
+
export function shouldNudge({ mutations, writes, touched, nudged, todos }: NudgeState): boolean {
|
|
84
|
+
if (nudged || mutations <= 0) return false;
|
|
85
|
+
if (hasOpenTodos(todos)) return true;
|
|
86
|
+
return todos.length === 0 && !touched && writes > 0 && mutations >= EMPTY_LIST_MUTATIONS;
|
|
45
87
|
}
|
|
46
88
|
|
|
47
89
|
/** Marker on the injected reminder, so it is recognizable and filterable. */
|
|
48
90
|
export const TODO_NUDGE_MARKER = "[TODO SYNC]";
|
|
49
91
|
|
|
50
92
|
/**
|
|
51
|
-
* Model-facing reminder injected before settlement.
|
|
52
|
-
*
|
|
53
|
-
* whole list
|
|
93
|
+
* Model-facing reminder injected before settlement. For a list with unfinished
|
|
94
|
+
* work it points at the item the list currently considers active, so the model
|
|
95
|
+
* can correct it without rereading the whole list; for no list at all it asks
|
|
96
|
+
* for one.
|
|
54
97
|
*/
|
|
55
98
|
export function formatNudge(todos: Todo[]): string {
|
|
99
|
+
if (todos.length === 0) {
|
|
100
|
+
return (
|
|
101
|
+
`${TODO_NUDGE_MARKER} Work happened but no todo list was recorded. ` +
|
|
102
|
+
"Call the todo tool with the steps of this work, then continue."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
56
105
|
const current = currentTodo(todos);
|
|
57
106
|
const item = current ? (current.activeForm ?? current.content) : undefined;
|
|
58
107
|
const pointer = item ? ` The list still points at "${item}".` : "";
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The list is reconstructed from the active branch on session start and tree
|
|
6
6
|
* navigation, and the persistent widget mirrors it whenever it changes. Only
|
|
7
|
-
* interactive (`tui`) sessions get a widget;
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* interactive (`tui`) sessions get a widget; whether a finished list stays
|
|
8
|
+
* visible comes from the todo config. The same runtime carries the lag-tracking
|
|
9
|
+
* counters the `agent_before_settle` reminder reads, so they reset with the
|
|
10
|
+
* session and the branch.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -27,8 +27,8 @@ export interface TodoRuntime {
|
|
|
27
27
|
reconstruct(ctx: ExtensionContext): void;
|
|
28
28
|
/** Apply the widget config (loaded per session). */
|
|
29
29
|
setConfig(config: TodoConfig): void;
|
|
30
|
-
/** Record
|
|
31
|
-
noteWork(): void;
|
|
30
|
+
/** Record a successful mutating call; `fileWrite` marks one that can change files. */
|
|
31
|
+
noteWork(fileWrite?: boolean): void;
|
|
32
32
|
/** Whether a lag reminder is due and has not been sent for it yet. */
|
|
33
33
|
nudgeDue(): boolean;
|
|
34
34
|
/** Mark the current lag as reminded, activating the injected reminder. */
|
|
@@ -53,11 +53,15 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
53
53
|
let lastTuiCtx: ExtensionContext | undefined;
|
|
54
54
|
// True while a dock screen owns the editor slot; the rail stays hidden.
|
|
55
55
|
let suppressed = false;
|
|
56
|
-
// Lag tracking: `
|
|
57
|
-
//
|
|
58
|
-
// `
|
|
59
|
-
//
|
|
60
|
-
|
|
56
|
+
// Lag tracking: `mutations` counts successful mutating calls since the list
|
|
57
|
+
// was last written and `writes` how many of those can change files,
|
|
58
|
+
// `touched` records that the model wrote or cleared the list this turn (so an
|
|
59
|
+
// explicit clear is not second-guessed), `nudged` caps the reminder to one per
|
|
60
|
+
// user turn, and `nudgeActive` marks an injected reminder as current so
|
|
61
|
+
// `context` keeps it for the continuation and drops it on the next user turn.
|
|
62
|
+
let mutations = 0;
|
|
63
|
+
let writes = 0;
|
|
64
|
+
let touched = false;
|
|
61
65
|
let nudged = false;
|
|
62
66
|
let nudgeActive = false;
|
|
63
67
|
// Screens watching the list (for example the open `/todos` screen) so they
|
|
@@ -104,13 +108,17 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
104
108
|
todos = next;
|
|
105
109
|
// A todo update is the model syncing the list, so the lag is cleared.
|
|
106
110
|
// `nudged` is not: the reminder stays capped at one per user turn.
|
|
107
|
-
|
|
111
|
+
mutations = 0;
|
|
112
|
+
writes = 0;
|
|
113
|
+
touched = true;
|
|
108
114
|
syncWidget(ctx);
|
|
109
115
|
notify();
|
|
110
116
|
},
|
|
111
117
|
reconstruct: (ctx) => {
|
|
112
118
|
todos = reconstructTodos(ctx.sessionManager.getBranch());
|
|
113
|
-
|
|
119
|
+
mutations = 0;
|
|
120
|
+
writes = 0;
|
|
121
|
+
touched = false;
|
|
114
122
|
nudged = false;
|
|
115
123
|
nudgeActive = false;
|
|
116
124
|
syncWidget(ctx);
|
|
@@ -119,10 +127,11 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
119
127
|
setConfig: (next) => {
|
|
120
128
|
config = next;
|
|
121
129
|
},
|
|
122
|
-
noteWork: () => {
|
|
123
|
-
|
|
130
|
+
noteWork: (fileWrite = false) => {
|
|
131
|
+
mutations++;
|
|
132
|
+
if (fileWrite) writes++;
|
|
124
133
|
},
|
|
125
|
-
nudgeDue: () => shouldNudge(
|
|
134
|
+
nudgeDue: () => shouldNudge({ mutations, writes, touched, nudged, todos }),
|
|
126
135
|
markNudged: () => {
|
|
127
136
|
nudged = true;
|
|
128
137
|
nudgeActive = true;
|
|
@@ -132,7 +141,9 @@ export function createTodoRuntime(pi?: Pick<ExtensionAPI, "events">): TodoRuntim
|
|
|
132
141
|
},
|
|
133
142
|
isNudgeActive: () => nudgeActive,
|
|
134
143
|
resetNudge: () => {
|
|
135
|
-
|
|
144
|
+
mutations = 0;
|
|
145
|
+
writes = 0;
|
|
146
|
+
touched = false;
|
|
136
147
|
nudged = false;
|
|
137
148
|
},
|
|
138
149
|
onChange: (listener) => {
|
|
@@ -29,7 +29,10 @@ How the `watch` call and its result look in the transcript — the call line as
|
|
|
29
29
|
- Shows a `⊚ N` running-watcher chip and a `! N` unreported-match chip, raises a
|
|
30
30
|
`ctx.ui.notify` alert once per unreported burst, and offers a `/watch` screen.
|
|
31
31
|
- Reports pending matches to the model at the start of the next turn, or wakes a
|
|
32
|
-
turn for a watcher added with `wake: true`.
|
|
32
|
+
turn for a watcher added with `wake: true`. A wake also leaves the terminal: the
|
|
33
|
+
match is announced on `my-pi-agent/watch-match`, which bytefly's
|
|
34
|
+
[notify](../bytefly/README.md#notifications) capability turns into a desktop
|
|
35
|
+
notification when `notify.enabled` and `notify.matchFound` are on.
|
|
33
36
|
- Watchers are session-only: `shutdown` flushes a partial line, kills every
|
|
34
37
|
command's process tree, and clears the chips.
|
|
35
38
|
|
|
@@ -162,12 +165,14 @@ other.
|
|
|
162
165
|
- `matches` and `logs` keep bounded histories (`maxMatches`/`maxLogLines`), so
|
|
163
166
|
older lines are dropped.
|
|
164
167
|
- Matching is per line; a pattern cannot span a newline.
|
|
168
|
+
- Only a **wake** match raises an OS notification, and only when bytefly's
|
|
169
|
+
`notify.enabled` and `notify.matchFound` are on. A match collected while the
|
|
170
|
+
agent was busy is reported at the next turn without a notification, and `watch`
|
|
171
|
+
itself never sends one — it announces the match and bytefly owns delivery.
|
|
165
172
|
|
|
166
173
|
## Non-goals
|
|
167
174
|
|
|
168
175
|
- Persisting or reattaching watchers across sessions.
|
|
169
|
-
- Notifying through bytefly's OS-notification capability (the `job` extension
|
|
170
|
-
owns that channel; `watch` alerts in-session with `ctx.ui.notify`).
|
|
171
176
|
- Multiline or stateful matching, log rotation hooks, and per-watcher poll
|
|
172
177
|
intervals.
|
|
173
178
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
|
+
import { WATCH_MATCH_CHANNEL, type WatchMatchEvent } from "../../lib/events.ts";
|
|
15
16
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
16
17
|
import { noticeMessage, type NoticeMessage, pruneStaleNotices } from "../../lib/notice.ts";
|
|
17
18
|
import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
|
|
@@ -80,6 +81,24 @@ export default function watch(pi: ExtensionAPI, deps: WatchDeps = {}): void {
|
|
|
80
81
|
waking = true;
|
|
81
82
|
} catch {
|
|
82
83
|
// Waking is best-effort; the next turn still reports the matches.
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// A wake means the user is away from the terminal, so the alert also leaves
|
|
87
|
+
// the process. bytefly's notify capability owns delivery; this publishes the
|
|
88
|
+
// fact and never learns how it is sent. The newest match is the one an alert
|
|
89
|
+
// is about, while the count covers the whole cycle.
|
|
90
|
+
const newest = pending[pending.length - 1];
|
|
91
|
+
if (!newest) return;
|
|
92
|
+
try {
|
|
93
|
+
const event: WatchMatchEvent = {
|
|
94
|
+
count: pending.length,
|
|
95
|
+
watcherId: newest.watcherId,
|
|
96
|
+
label: newest.label,
|
|
97
|
+
line: newest.line,
|
|
98
|
+
};
|
|
99
|
+
pi.events.emit(WATCH_MATCH_CHANNEL, event);
|
|
100
|
+
} catch {
|
|
101
|
+
// A throwing listener must not cost the wake that already happened.
|
|
83
102
|
}
|
|
84
103
|
};
|
|
85
104
|
|
|
@@ -28,6 +28,9 @@ How the `web_search` and `web_fetch` calls and their results look in the transcr
|
|
|
28
28
|
always with no configuration required.
|
|
29
29
|
- Registers `web_fetch`, which fetches a URL (GET or POST) and
|
|
30
30
|
returns readable text with links kept as `[text](url)`.
|
|
31
|
+
- Decodes by the byte-order mark, the declared `Content-Type` charset, or the
|
|
32
|
+
page's own `<meta charset>`, so a GBK/GB18030 page reads as text instead of
|
|
33
|
+
replacement characters.
|
|
31
34
|
- Pages long output and reports the next `startIndex` (`nextIndex` in
|
|
32
35
|
`structuredContent`); `find` returns matching passages instead of the whole
|
|
33
36
|
page, and says when the match cap hid further hits.
|
|
@@ -96,7 +99,7 @@ and unknown keys are ignored.
|
|
|
96
99
|
{
|
|
97
100
|
"search": {
|
|
98
101
|
"provider": "auto", // "auto" | "duckduckgo" | "searxng" | "brave"
|
|
99
|
-
"endpoint": "
|
|
102
|
+
"endpoint": "", // SearXNG base URL, e.g. "http://localhost:8080"; "" means SearXNG is unavailable
|
|
100
103
|
"language": "auto", // SearXNG: "auto" (zh-CN for Han, else en) | language code
|
|
101
104
|
"safeSearch": 0, // SearXNG: 0 off, 1 moderate, 2 strict
|
|
102
105
|
"categories": "general", // SearXNG categories
|
|
@@ -148,6 +151,8 @@ as `Authorization: Bearer <key>`; for Brave it is sent as
|
|
|
148
151
|
cannot redirect into a private address.
|
|
149
152
|
- Transport headers (`Host`, `Content-Length`, `Connection`, `Transfer-Encoding`,
|
|
150
153
|
and similar) are refused, and request bodies are capped.
|
|
154
|
+
- `maxBytes` is enforced while the body streams in: a chunked or compressed
|
|
155
|
+
response is abandoned at the limit instead of being buffered whole.
|
|
151
156
|
|
|
152
157
|
## `web_search`
|
|
153
158
|
|
|
@@ -173,6 +178,13 @@ The SearXNG instance must have the **JSON API enabled** (`format=json` in its
|
|
|
173
178
|
`settings.yml`); most public instances disable it, so a self-hosted instance is
|
|
174
179
|
recommended. If the response is not JSON, the error says so.
|
|
175
180
|
|
|
181
|
+
`run_doctor` reports what this session actually resolved to — the provider id and
|
|
182
|
+
whether it came from an explicit choice, the `auto` rule, or the keyless default;
|
|
183
|
+
the API-key variable when the provider takes one and no literal `apiKey` is set,
|
|
184
|
+
and whether that variable is present; and, only on an explicit `refresh`, whether
|
|
185
|
+
the configured `endpoint` answers. The key is never reported, only its variable
|
|
186
|
+
name. See [`bytefly`](../bytefly/README.md#what-it-does) for the report.
|
|
187
|
+
|
|
176
188
|
| Parameter | Type | Notes |
|
|
177
189
|
|---|---|---|
|
|
178
190
|
| `query` | string | 1–400 chars, required, any language |
|
|
@@ -200,7 +212,7 @@ address. Loopback, private, link-local, and cloud-metadata addresses are refused
|
|
|
200
212
|
| `headers` | object | Extra request headers (transport headers are refused) |
|
|
201
213
|
| `body` | string | POST body; implies `method: "POST"` unless set explicitly |
|
|
202
214
|
| `startIndex` | integer | Code-point offset to start from (default 0) |
|
|
203
|
-
| `maxChars` | integer | ≥200; capped by `maxOutputChars` |
|
|
215
|
+
| `maxChars` | integer | ≥200; capped by `maxOutputChars`; also caps `find` output |
|
|
204
216
|
| `find` | string[] | 1–10 strings; return matching passages instead of the page |
|
|
205
217
|
| `mode` | enum | `insensitive` (default) \| `exact` \| `fuzzy` |
|
|
206
218
|
| `contextChars` | integer | 0–2000 chars of context per match (default 200) |
|
|
@@ -209,10 +221,14 @@ address. Loopback, private, link-local, and cloud-metadata addresses are refused
|
|
|
209
221
|
|
|
210
222
|
Long pages are paged: a truncated page names the `startIndex` to use next, inside
|
|
211
223
|
its own character budget, and `structuredContent` repeats it as `nextIndex`.
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
A `find` result spends the same `maxChars` budget: passages are added while they
|
|
225
|
+
fit, and the note names `maxMatches` when the match cap hid the rest or
|
|
226
|
+
`maxChars`/`contextChars` when the budget did. Fetched pages are cached for the
|
|
227
|
+
registration, so paging and `find` do not refetch. POST responses and requests
|
|
228
|
+
with custom headers are never cached.
|
|
214
229
|
`structuredContent` is the page object, with an `error` field when the fetch
|
|
215
|
-
failed and `matchesTruncated` when
|
|
230
|
+
failed and `matchesTruncated` when the match cap or the `maxChars` budget hid
|
|
231
|
+
matches.
|
|
216
232
|
|
|
217
233
|
```
|
|
218
234
|
web_fetch({ url: "https://pi.dev/" })
|
|
@@ -243,6 +259,9 @@ the content type and size.
|
|
|
243
259
|
- **Heuristic extraction**: `web_fetch` strips scripts/styles/nav/footer and
|
|
244
260
|
prefers `<main>`/`<article>`, but is not a full readability engine. PDF
|
|
245
261
|
extraction is optional (`unpdf`) and off unless installed.
|
|
262
|
+
- **`find` output is budgeted.** With `maxMatches: 50` and `contextChars: 2000`
|
|
263
|
+
the passages alone would exceed `maxOutputChars`; the result shows the passages
|
|
264
|
+
that fit and names the knob to change.
|
|
246
265
|
- **The page cache is in-memory and per registration**; a page can be stale
|
|
247
266
|
within a session. `refresh: true` refetches, and it is cleared on
|
|
248
267
|
`session_start` and `session_shutdown`.
|
|
@@ -264,9 +283,9 @@ the content type and size.
|
|
|
264
283
|
|
|
265
284
|
| Integration point | Value |
|
|
266
285
|
|---|---|
|
|
267
|
-
| Tools | `web_search`, `web_fetch`;
|
|
286
|
+
| Tools | `web_search`, `web_fetch`; direct exposure, active on registration (the defaults, so not declared) |
|
|
268
287
|
| Execution mode | `web_search`: `sequential`; `web_fetch`: default |
|
|
269
|
-
| Annotations | both `readOnlyHint: true`, `openWorldHint: true
|
|
288
|
+
| Annotations | both `readOnlyHint: true`, `openWorldHint: true`; a read-only tool declares no `destructiveHint` |
|
|
270
289
|
| Output | `outputSchema` (`WebSearchOutput` / `WebFetchOutput`) plus matching `structuredContent` |
|
|
271
290
|
| State | none in the session branch; one fetch page cache per registration (`web_search` keeps a per-instance throttle) |
|
|
272
291
|
| Lifecycle | the factory only registers (`index.ts`); `registerFetchTool` owns the page cache and clears it on `session_start` and idempotent `session_shutdown` |
|
|
@@ -279,7 +298,12 @@ the content type and size.
|
|
|
279
298
|
live at the top level (`text.ts`, `html.ts`, `transcript.ts`, `http.ts`).
|
|
280
299
|
- **Shared transport.** [`http.ts`](./http.ts) holds the fetch runner
|
|
281
300
|
(`HttpRunner`, `createFetchRunner`, HTTP error types), used by both halves and
|
|
282
|
-
injectable for tests.
|
|
301
|
+
injectable for tests. It reads the body through a byte-counting stream, so
|
|
302
|
+
`maxBytes` stops a chunked or compressed response before it is buffered whole.
|
|
303
|
+
- **Charset-aware decoding.** [`charset.ts`](./charset.ts) is the one place a
|
|
304
|
+
byte body becomes text: BOM first, then the declared `Content-Type` charset,
|
|
305
|
+
then the page's own `<meta charset>`, then UTF-8. An unknown label falls back
|
|
306
|
+
to UTF-8 instead of failing the fetch.
|
|
283
307
|
- **One config file, isolated sections.** `config.ts` reads the global then
|
|
284
308
|
project file and validates each section with its half's normalizer; unknown
|
|
285
309
|
keys and malformed files are ignored.
|
|
@@ -302,7 +326,11 @@ the content type and size.
|
|
|
302
326
|
mutable is held at module scope.
|
|
303
327
|
- **The paging budget is explicit.** `formatPage` counts the header and the
|
|
304
328
|
truncation note against `maxChars`, so a paged page always carries its
|
|
305
|
-
`startIndex` hint inside the limit.
|
|
329
|
+
`startIndex` hint inside the limit. `formatMatches` applies the same rule to
|
|
330
|
+
`find`: it includes the passages that fit and names the knob that hid the rest.
|
|
331
|
+
- **Code points without a copy.** [`text.ts`](./text.ts) counts and slices by
|
|
332
|
+
walking the string in place, so a multi-megabyte page is never expanded into an
|
|
333
|
+
array of one-character strings on every call.
|
|
306
334
|
- **One JSON boundary per concern.** `search/json.ts` owns `requestJson` (status
|
|
307
335
|
check and parse error) and `collectResults` (title + http(s) URL + de-dupe), so
|
|
308
336
|
the three providers cannot drift on either.
|
|
@@ -321,7 +349,8 @@ the content type and size.
|
|
|
321
349
|
|---|---|
|
|
322
350
|
| `index.ts` | Register both tools |
|
|
323
351
|
| `config.ts` | The `web-access` section of `bytefly.json` loader (via `lib/config.ts`) |
|
|
324
|
-
| `http.ts` | Shared fetch runner and HTTP error types |
|
|
352
|
+
| `http.ts` | Shared fetch runner and HTTP error types; caps the body while streaming |
|
|
353
|
+
| `charset.ts` | Charset-aware body decoding (BOM, header, `<meta>`, UTF-8 fallback) |
|
|
325
354
|
| `text.ts` | Shared code-point length, slice, and truncation helpers |
|
|
326
355
|
| `html.ts` | Shared HTML entity decoding |
|
|
327
356
|
| `transcript.ts` | Shared `oneLine` sanitizer for both transcript renderers |
|
|
@@ -353,11 +382,12 @@ the content type and size.
|
|
|
353
382
|
`config.test.ts` and `index.test.ts`. It covers the shared runner (via injected
|
|
354
383
|
`fetch`), the provider registry and resolution, each provider's URL builder and
|
|
355
384
|
parser (DuckDuckGo, SearXNG, Brave), the orchestration pipeline and throttle,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
registration
|
|
385
|
+
charset decoding (declared, `<meta>`, and BOM precedence), a streamed body over
|
|
386
|
+
the byte cap, redirect re-validation (per-hop SSRF with literal IPs),
|
|
387
|
+
method/header/body validation, the nested config merge and section isolation,
|
|
388
|
+
extraction, formatting/paging through the tool (including a regression test that
|
|
389
|
+
a truncated page keeps its `startIndex` hint), find and its cap/budget notices,
|
|
390
|
+
the per-registration cache, the SSRF guard, the injected PDF dependency, and both
|
|
391
|
+
tools' registration/execution. Tests inject the runner, throttle, and PDF extractor
|
|
362
392
|
through the tools' dependencies, so no network or PDF library is needed; the
|
|
363
393
|
runtime smoke test only asserts registration.
|