one-code-extension 0.1.4 → 0.3.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/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* invisibly, so the fallback is loud, never silent.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
14
|
+
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
15
15
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
16
16
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { recordUsage } from "../lib/usage-bus.ts";
|
|
17
18
|
import { Type } from "typebox";
|
|
18
19
|
import { DEFER_CHANNEL } from "../lib/deferred.ts";
|
|
20
|
+
import { withReasoningFallback } from "../lib/model-policy.ts";
|
|
19
21
|
import { htmlToMarkdown, isSameHost, normalizeUrl, paginate } from "./extract.ts";
|
|
20
22
|
import { pickReaderModel, READER_MAX_TOKENS, readerMessages } from "./summarize.ts";
|
|
21
23
|
import { ccToolRenderers } from "../lib/tui-render.ts";
|
|
@@ -23,6 +25,12 @@ import { ccToolRenderers } from "../lib/tui-render.ts";
|
|
|
23
25
|
const DEFAULT_MAX_CHARS = 30_000;
|
|
24
26
|
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
25
27
|
const FETCH_TIMEOUT_MS = 30_000;
|
|
28
|
+
/** Same-host redirect hops followed before giving up (a redirect loop is otherwise unbounded recursion). */
|
|
29
|
+
const MAX_REDIRECTS = 5;
|
|
30
|
+
/** Response bytes read before the body is cut off — a multi-GB "page" must not be buffered whole. */
|
|
31
|
+
const MAX_BODY_BYTES = 5_000_000;
|
|
32
|
+
/** Cache entries kept; the oldest is evicted past this (the TTL alone never evicted). */
|
|
33
|
+
const MAX_CACHE_ENTRIES = 50;
|
|
26
34
|
/** Longer than the classifier's cap: the reader ingests whole pages. */
|
|
27
35
|
const READER_TIMEOUT_MS = 60_000;
|
|
28
36
|
const USER_AGENT = "one-code/0.1 (+https://github.com/IsuruMaduranga/one-code)";
|
|
@@ -60,6 +68,11 @@ async function answerFromPage(
|
|
|
60
68
|
entry: { markdown: string; title?: string },
|
|
61
69
|
url: string,
|
|
62
70
|
signal: AbortSignal | undefined,
|
|
71
|
+
/** Per-session memo of a reader model's required thinking level, so a reader that
|
|
72
|
+
* cannot disable thinking pays the mandatory-thinking 400 once, not per fetch. */
|
|
73
|
+
learnedReasoning: Map<string, ThinkingLevel>,
|
|
74
|
+
/** Report the reader call's usage for the all-in footer cost. */
|
|
75
|
+
recordCall: (usage: unknown) => void,
|
|
63
76
|
): Promise<{ answer?: string; reader?: string; truncated?: boolean; error?: string }> {
|
|
64
77
|
const choice = pickReaderModel(ctx.modelRegistry.getAvailable(), ctx.model);
|
|
65
78
|
if (!choice) return { error: "no model available to read the page" };
|
|
@@ -71,20 +84,31 @@ async function answerFromPage(
|
|
|
71
84
|
const baseUrl = (auth as { baseUrl?: string }).baseUrl;
|
|
72
85
|
|
|
73
86
|
const messages = readerMessages({ prompt, markdown: entry.markdown, url, title: entry.title });
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
// Thinking off unless the model cannot disable it; withReasoningFallback sends
|
|
88
|
+
// a level up front for catalog-marked models and retries on the 400 for the
|
|
89
|
+
// rest, memoizing the result so repeated fetches don't re-pay the failure.
|
|
90
|
+
const result = await withReasoningFallback(
|
|
91
|
+
choice.model,
|
|
92
|
+
(reasoning) => {
|
|
93
|
+
const timeout = AbortSignal.timeout(READER_TIMEOUT_MS);
|
|
94
|
+
return completeSimple(
|
|
95
|
+
baseUrl ? ({ ...choice.model, baseUrl } as Model<Api>) : choice.model,
|
|
96
|
+
{
|
|
97
|
+
systemPrompt: messages.system,
|
|
98
|
+
messages: [{ role: "user", content: messages.user, timestamp: Date.now() }],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
apiKey: auth.apiKey,
|
|
102
|
+
headers: auth.headers,
|
|
103
|
+
env: auth.env,
|
|
104
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
105
|
+
maxTokens: READER_MAX_TOKENS,
|
|
106
|
+
...(reasoning ? { reasoning } : {}),
|
|
107
|
+
},
|
|
108
|
+
);
|
|
87
109
|
},
|
|
110
|
+
learnedReasoning,
|
|
111
|
+
recordCall,
|
|
88
112
|
);
|
|
89
113
|
const answer = result.content
|
|
90
114
|
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
@@ -98,12 +122,35 @@ async function answerFromPage(
|
|
|
98
122
|
}
|
|
99
123
|
}
|
|
100
124
|
|
|
125
|
+
/** Read a response body as text, stopping after `cap` bytes (the stream is cancelled, not drained). */
|
|
126
|
+
async function readBodyCapped(response: Response, cap: number): Promise<{ text: string; truncated: boolean }> {
|
|
127
|
+
const reader = response.body?.getReader();
|
|
128
|
+
if (!reader) return { text: await response.text(), truncated: false };
|
|
129
|
+
const decoder = new TextDecoder();
|
|
130
|
+
let text = "";
|
|
131
|
+
let bytes = 0;
|
|
132
|
+
while (true) {
|
|
133
|
+
const { done, value } = await reader.read();
|
|
134
|
+
if (done) break;
|
|
135
|
+
bytes += value.byteLength;
|
|
136
|
+
if (bytes > cap) {
|
|
137
|
+
text += decoder.decode(value.subarray(0, Math.max(0, value.byteLength - (bytes - cap))), { stream: true });
|
|
138
|
+
await reader.cancel().catch(() => {});
|
|
139
|
+
return { text: text + decoder.decode(), truncated: true };
|
|
140
|
+
}
|
|
141
|
+
text += decoder.decode(value, { stream: true });
|
|
142
|
+
}
|
|
143
|
+
return { text: text + decoder.decode(), truncated: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
101
146
|
export default function webFetchExtension(pi: ExtensionAPI) {
|
|
102
147
|
// Same 15-minute window Claude Code documents, so repeated reads of one page
|
|
103
148
|
// during a task don't re-download it.
|
|
104
149
|
const cache = new Map<string, CacheEntry>();
|
|
150
|
+
// Per-session: a reader model's learned required thinking level (see answerFromPage).
|
|
151
|
+
const learnedReasoning = new Map<string, ThinkingLevel>();
|
|
105
152
|
|
|
106
|
-
const load = async (url: string, signal: AbortSignal | undefined): Promise<CacheEntry> => {
|
|
153
|
+
const load = async (url: string, signal: AbortSignal | undefined, redirects = 0): Promise<CacheEntry> => {
|
|
107
154
|
const cached = cache.get(url);
|
|
108
155
|
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) return cached;
|
|
109
156
|
|
|
@@ -127,7 +174,8 @@ export default function webFetchExtension(pi: ExtensionAPI) {
|
|
|
127
174
|
if (location) {
|
|
128
175
|
const target = new URL(location, url).toString();
|
|
129
176
|
if (isSameHost(target, url)) {
|
|
130
|
-
|
|
177
|
+
if (redirects >= MAX_REDIRECTS) throw new Error(`Too many redirects (${MAX_REDIRECTS}); last target ${target}`);
|
|
178
|
+
return await load(target, signal, redirects + 1);
|
|
131
179
|
}
|
|
132
180
|
throw new Error(`Redirects to a different host: ${target}\nCall web_fetch again with that URL if you want it.`);
|
|
133
181
|
}
|
|
@@ -138,7 +186,7 @@ export default function webFetchExtension(pi: ExtensionAPI) {
|
|
|
138
186
|
}
|
|
139
187
|
|
|
140
188
|
const contentType = response.headers.get("content-type") ?? "";
|
|
141
|
-
const body = await response
|
|
189
|
+
const { text: body, truncated } = await readBodyCapped(response, MAX_BODY_BYTES);
|
|
142
190
|
|
|
143
191
|
let entry: CacheEntry;
|
|
144
192
|
if (contentType.includes("html")) {
|
|
@@ -155,7 +203,13 @@ export default function webFetchExtension(pi: ExtensionAPI) {
|
|
|
155
203
|
entry = { markdown: body.trim(), fetchedAt: Date.now() };
|
|
156
204
|
}
|
|
157
205
|
|
|
206
|
+
if (truncated) entry.note = [entry.note, `The response was cut at ${MAX_BODY_BYTES / 1_000_000} MB.`].filter(Boolean).join(" ");
|
|
158
207
|
cache.set(url, entry);
|
|
208
|
+
while (cache.size > MAX_CACHE_ENTRIES) {
|
|
209
|
+
const oldest = cache.keys().next().value;
|
|
210
|
+
if (oldest === undefined) break;
|
|
211
|
+
cache.delete(oldest);
|
|
212
|
+
}
|
|
159
213
|
return entry;
|
|
160
214
|
} finally {
|
|
161
215
|
clearTimeout(timer);
|
|
@@ -206,7 +260,9 @@ export default function webFetchExtension(pi: ExtensionAPI) {
|
|
|
206
260
|
// readerNote, never silently.
|
|
207
261
|
let readerNote: string | undefined;
|
|
208
262
|
if (params.prompt) {
|
|
209
|
-
const answered = await answerFromPage(ctx, params.prompt, entry, target, signal)
|
|
263
|
+
const answered = await answerFromPage(ctx, params.prompt, entry, target, signal, learnedReasoning, (usage) =>
|
|
264
|
+
recordUsage(pi, "reader", usage),
|
|
265
|
+
);
|
|
210
266
|
if (answered.answer !== undefined) {
|
|
211
267
|
const header = [
|
|
212
268
|
entry.title ? `# ${entry.title}` : undefined,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
17
|
-
import { type EconomicalModelChoice, pickEconomicalContainedModel } from "../lib/model-
|
|
17
|
+
import { type EconomicalModelChoice, pickEconomicalContainedModel } from "../lib/model-tier.ts";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* How much of the page the reader sees. Far past the 30k-char window the main
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* In-process subagent execution — the one place the workflow extension
|
|
3
|
-
* touches pi's SDK. Each agent() call becomes a real
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* touches pi's SDK. Each agent() call becomes a real agent session with an
|
|
4
|
+
* in-memory session file; the run shares a single ModelRuntime across all its
|
|
5
|
+
* agents (building one per agent is expensive). The resource loader is NOT
|
|
6
|
+
* shared: disposing one session on a loader invalidates the loader's runtime
|
|
7
|
+
* for every other session built on it (lib/agent-loader.ts, LIFECYCLE-REVIEW
|
|
8
|
+
* H3), so each agent() call opens its own through `openChildSession`, which
|
|
9
|
+
* also emits the `session_start` pi's SDK never sends (H2).
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Each loader uses `noExtensions: true`, which structurally blocks recursive
|
|
12
|
+
* orchestration (no workflow tool inside subagents) — and would also drop One
|
|
13
|
+
* Code's permission gate, so `permissionGateFactory` is passed via
|
|
14
|
+
* `extensionFactories`, which DefaultResourceLoader always loads.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import os from "node:os";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
createAgentSession,
|
|
18
|
-
DefaultResourceLoader,
|
|
19
|
-
getAgentDir,
|
|
20
|
-
ModelRuntime,
|
|
21
|
-
SessionManager,
|
|
22
|
-
type ToolDefinition,
|
|
23
|
-
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { getAgentDir, type ModelRuntime, SessionManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
24
19
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
25
20
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
26
21
|
import { Type } from "typebox";
|
|
22
|
+
import { whenAborted } from "../lib/abort.ts";
|
|
23
|
+
import { createSharedModelRuntime, finalAssistantText, openChildSession } from "../lib/agent-loader.ts";
|
|
24
|
+
import { modelSpec as modelSpecOf } from "../lib/model-policy.ts";
|
|
25
|
+
import { isModelUnavailableError } from "../auto-mode/model-select.ts";
|
|
26
|
+
import { agentPromptIdentity, PrefixWarmGate, prefixWarmKey } from "../lib/prefix-warm-gate.ts";
|
|
27
|
+
import type { PermissionBridge } from "../permissions/subagent-gate.ts";
|
|
28
|
+
import { localGateMode, MODE_ENV } from "../lib/permission-gate.ts";
|
|
29
|
+
import type { HookBridge } from "../hooks/subagent-bridge.ts";
|
|
30
|
+
import { summarizeArgs } from "../lib/tui-render.ts";
|
|
27
31
|
import { agentDirs, type AgentDefinition, discoverAgents } from "../subagents/agents.ts";
|
|
32
|
+
import type { SubagentDefault } from "../subagents/default-model.ts";
|
|
28
33
|
import { expensiveModelGate, resolveSubagentModel, subagentModelMenu } from "../subagents/model-select.ts";
|
|
34
|
+
import { withoutUnusable } from "../lib/model-unusable.ts";
|
|
29
35
|
import { cleanupWorktree, createWorktree, isGitRepo, type Worktree } from "../subagents/worktree.ts";
|
|
30
|
-
import {
|
|
31
|
-
import type { AgentCallOptions, AgentCallResult, AgentEffort } from "./types.ts";
|
|
36
|
+
import type { AgentCallOptions, AgentCallResult, AgentEffort, AgentRunUpdate } from "./types.ts";
|
|
32
37
|
import { WorkflowScriptError } from "./types.ts";
|
|
33
38
|
|
|
34
39
|
const MAX_SCHEMA_RETRIES = 2;
|
|
@@ -38,11 +43,69 @@ export interface AgentRunnerOptions {
|
|
|
38
43
|
cwd: string;
|
|
39
44
|
/** Session model, retained as the final fallback. */
|
|
40
45
|
defaultModel: unknown;
|
|
41
|
-
/** `/subagent` user/managed default; automatic role selection follows it. */
|
|
42
|
-
|
|
46
|
+
/** `/subagent` user/managed default (with its staleness stamp); automatic role selection follows it. */
|
|
47
|
+
configuredDefault?: SubagentDefault;
|
|
43
48
|
defaultEffort?: AgentEffort | string;
|
|
44
49
|
/** Surface model-resolution notices (fallbacks, provider crossings) in the run log. */
|
|
45
50
|
onNotice?: (message: string) => void;
|
|
51
|
+
/** Models this account cannot run (`provider/id`), learned this session — dropped from the catalog before resolving (lib/model-unusable.ts). */
|
|
52
|
+
unusableModels?: () => ReadonlySet<string>;
|
|
53
|
+
/** An agent's provider refused its model as not usable on this account (`isModelUnavailableError`): report it so every picker skips it. */
|
|
54
|
+
onModelUnusable?: (model: string, reason: string) => void;
|
|
55
|
+
/**
|
|
56
|
+
* The parent permissions extension's decision closure (same bridge the
|
|
57
|
+
* subagent runner uses): when present, a workflow agent's tool calls route
|
|
58
|
+
* through the parent's real permission pipeline — mode inheritance, the
|
|
59
|
+
* auto-mode classifier, prompts bubbled to the user. Without it the child
|
|
60
|
+
* gate falls back to fail-closed local rules, which deny every bash call
|
|
61
|
+
* that would normally ask — unusable outside auto/bypass modes.
|
|
62
|
+
*/
|
|
63
|
+
getPermissionBridge?: () => PermissionBridge | undefined;
|
|
64
|
+
/** The parent hooks extension's bridge, so the user's tool hooks run for workflow agents too. */
|
|
65
|
+
getHookBridge?: () => HookBridge | undefined;
|
|
66
|
+
/** Each finished agent's dollar cost (review S13: workflow agents never reached the footer). */
|
|
67
|
+
onUsage?: (cost: number) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Judge one `agent()` prompt as a delegation before the child starts — the
|
|
72
|
+
* same `Agent` check the main conversation's spawns and a subagent's nested
|
|
73
|
+
* spawns go through (matcher DELEGATION_TOOLS; subagents/index.ts). The one
|
|
74
|
+
* `workflow` call was classified when the script was submitted, but every
|
|
75
|
+
* prompt is computed inside the vm afterwards, often from an earlier agent's
|
|
76
|
+
* output, and until 2026-09-05 none of them was judged
|
|
77
|
+
* (PERMISSIONS-REVIEW-2026-09-05 L4). Outside auto mode the bridge allows at
|
|
78
|
+
* once (Agent is auto-allowed there unless a deny rule names it). Without a
|
|
79
|
+
* bridge — no publishing parent — the run is allowed unless the live mode is
|
|
80
|
+
* auto, whose classifier is only reachable through the parent; then it fails
|
|
81
|
+
* closed. A refusal is thrown as a plain Error, i.e. an agent failure (the
|
|
82
|
+
* call resolves null, CC semantics), not a WorkflowScriptError: the script's
|
|
83
|
+
* other agents still run.
|
|
84
|
+
*/
|
|
85
|
+
export async function judgeWorkflowDelegation(input: {
|
|
86
|
+
bridge: PermissionBridge | undefined;
|
|
87
|
+
liveMode: string;
|
|
88
|
+
prompt: string;
|
|
89
|
+
agentType?: string;
|
|
90
|
+
label?: string;
|
|
91
|
+
cwd: string;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}): Promise<void> {
|
|
94
|
+
if (!input.bridge) {
|
|
95
|
+
if (input.liveMode === "auto") {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"agent() refused: auto mode's classifier is only reachable through the parent session, which this run has no link to",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const verdict = await input.bridge({
|
|
103
|
+
toolName: "Agent",
|
|
104
|
+
input: { subagent_type: input.agentType, prompt: input.prompt, description: input.label },
|
|
105
|
+
cwd: input.cwd,
|
|
106
|
+
signal: input.signal,
|
|
107
|
+
});
|
|
108
|
+
if (verdict?.block) throw new Error(`agent() refused by the permission gate: ${verdict.reason}`);
|
|
46
109
|
}
|
|
47
110
|
|
|
48
111
|
/**
|
|
@@ -54,7 +117,7 @@ const THINKING_SUFFIX = /^(off|minimal|low|medium|high|xhigh|max)$/i;
|
|
|
54
117
|
export interface WorkflowModelInput {
|
|
55
118
|
opts: AgentCallOptions;
|
|
56
119
|
agentModel?: string;
|
|
57
|
-
|
|
120
|
+
configuredDefault?: SubagentDefault;
|
|
58
121
|
sessionModel?: Model<Api>;
|
|
59
122
|
available: Model<Api>[];
|
|
60
123
|
defaultEffort?: string;
|
|
@@ -79,13 +142,13 @@ export function resolveWorkflowAgentModel(input: WorkflowModelInput): {
|
|
|
79
142
|
const resolution = resolveSubagentModel({
|
|
80
143
|
requested,
|
|
81
144
|
agentModel: input.agentModel,
|
|
82
|
-
configuredDefault: input.
|
|
145
|
+
configuredDefault: input.configuredDefault,
|
|
83
146
|
sessionModel: input.sessionModel,
|
|
84
147
|
available: input.available,
|
|
85
148
|
});
|
|
86
149
|
if (resolution.unresolved) {
|
|
87
150
|
const fallback = resolveSubagentModel({
|
|
88
|
-
configuredDefault: input.
|
|
151
|
+
configuredDefault: input.configuredDefault,
|
|
89
152
|
sessionModel: input.sessionModel,
|
|
90
153
|
available: input.available,
|
|
91
154
|
});
|
|
@@ -115,43 +178,28 @@ export function resolveWorkflowAgentModel(input: WorkflowModelInput): {
|
|
|
115
178
|
export class AgentRunner {
|
|
116
179
|
private readonly options: AgentRunnerOptions;
|
|
117
180
|
private readonly modelRuntime: ModelRuntime;
|
|
118
|
-
private readonly loader: DefaultResourceLoader;
|
|
119
181
|
private readonly agentCatalog: AgentDefinition[];
|
|
120
182
|
private readonly availableModels: Model<Api>[];
|
|
121
|
-
/**
|
|
122
|
-
*
|
|
123
|
-
|
|
183
|
+
/**
|
|
184
|
+
* `parallel()` children of one agent type share a request prefix; the first
|
|
185
|
+
* starts streaming before the rest go, so the fan-out writes the prefix once
|
|
186
|
+
* and reads it N-1 times (lib/prefix-warm-gate.ts).
|
|
187
|
+
*/
|
|
188
|
+
private readonly warmGate = new PrefixWarmGate();
|
|
124
189
|
|
|
125
|
-
private constructor(
|
|
126
|
-
options: AgentRunnerOptions,
|
|
127
|
-
modelRuntime: ModelRuntime,
|
|
128
|
-
loader: DefaultResourceLoader,
|
|
129
|
-
agentCatalog: AgentDefinition[],
|
|
130
|
-
availableModels: Model<Api>[],
|
|
131
|
-
) {
|
|
190
|
+
private constructor(options: AgentRunnerOptions, modelRuntime: ModelRuntime, agentCatalog: AgentDefinition[], availableModels: Model<Api>[]) {
|
|
132
191
|
this.options = options;
|
|
133
192
|
this.modelRuntime = modelRuntime;
|
|
134
|
-
this.loader = loader;
|
|
135
193
|
this.agentCatalog = agentCatalog;
|
|
136
194
|
this.availableModels = availableModels;
|
|
137
195
|
}
|
|
138
196
|
|
|
139
197
|
static async create(options: AgentRunnerOptions): Promise<AgentRunner> {
|
|
140
198
|
const agentDir = getAgentDir();
|
|
141
|
-
const modelRuntime = await
|
|
142
|
-
authPath: join(agentDir, "auth.json"),
|
|
143
|
-
modelsPath: join(agentDir, "models.json"),
|
|
144
|
-
});
|
|
145
|
-
const loader = new DefaultResourceLoader({
|
|
146
|
-
cwd: options.cwd,
|
|
147
|
-
agentDir,
|
|
148
|
-
noExtensions: true,
|
|
149
|
-
extensionFactories: [permissionGateFactory(options.cwd, os.homedir())],
|
|
150
|
-
});
|
|
151
|
-
await loader.reload();
|
|
199
|
+
const modelRuntime = await createSharedModelRuntime(agentDir);
|
|
152
200
|
const agentCatalog = discoverAgents(agentDirs(options.cwd, os.homedir()));
|
|
153
201
|
const availableModels = [...(await modelRuntime.getAvailable())];
|
|
154
|
-
return new AgentRunner(options, modelRuntime,
|
|
202
|
+
return new AgentRunner(options, modelRuntime, agentCatalog, availableModels);
|
|
155
203
|
}
|
|
156
204
|
|
|
157
205
|
/**
|
|
@@ -165,16 +213,21 @@ export class AgentRunner {
|
|
|
165
213
|
const resolution = resolveWorkflowAgentModel({
|
|
166
214
|
opts,
|
|
167
215
|
agentModel,
|
|
168
|
-
|
|
216
|
+
configuredDefault: this.options.configuredDefault,
|
|
169
217
|
sessionModel: this.options.defaultModel as Model<Api> | undefined,
|
|
170
|
-
available: this.availableModels,
|
|
218
|
+
available: withoutUnusable(this.availableModels, this.options.unusableModels?.() ?? new Set()),
|
|
171
219
|
defaultEffort: this.options.defaultEffort as string | undefined,
|
|
172
220
|
});
|
|
173
221
|
for (const notice of resolution.notices) this.options.onNotice?.(notice);
|
|
174
222
|
return { model: resolution.model ?? this.options.defaultModel, thinkingLevel: resolution.thinkingLevel };
|
|
175
223
|
}
|
|
176
224
|
|
|
177
|
-
async run(
|
|
225
|
+
async run(
|
|
226
|
+
prompt: string,
|
|
227
|
+
opts: AgentCallOptions,
|
|
228
|
+
signal: AbortSignal,
|
|
229
|
+
onUpdate?: (update: AgentRunUpdate) => void,
|
|
230
|
+
): Promise<AgentCallResult> {
|
|
178
231
|
let agentDef: AgentDefinition | undefined;
|
|
179
232
|
if (opts.agentType) {
|
|
180
233
|
agentDef = this.agentCatalog.find((a) => a.name === opts.agentType);
|
|
@@ -185,6 +238,10 @@ export class AgentRunner {
|
|
|
185
238
|
}
|
|
186
239
|
|
|
187
240
|
const modelSpec = this.resolveModel(opts, agentDef?.model);
|
|
241
|
+
const resolvedModel = modelSpec.model as Model<Api> | undefined;
|
|
242
|
+
if (resolvedModel?.id) onUpdate?.({ model: resolvedModel.id });
|
|
243
|
+
|
|
244
|
+
await this.judgeDelegation(prompt, opts, agentDef, signal);
|
|
188
245
|
|
|
189
246
|
let worktree: Worktree | undefined;
|
|
190
247
|
let cwd = this.options.cwd;
|
|
@@ -199,22 +256,58 @@ export class AgentRunner {
|
|
|
199
256
|
const capture: { called: boolean; value: unknown } = { called: false, value: undefined };
|
|
200
257
|
const customTools: ToolDefinition[] = opts.schema ? [buildStructuredOutputTool(opts.schema, capture)] : [];
|
|
201
258
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
259
|
+
const session = await openChildSession({
|
|
260
|
+
loader: {
|
|
261
|
+
cwd: this.options.cwd,
|
|
262
|
+
agentDir: getAgentDir(),
|
|
263
|
+
systemPrompt: agentDef?.systemPrompt,
|
|
264
|
+
getPermissionBridge: this.options.getPermissionBridge,
|
|
265
|
+
getHookBridge: this.options.getHookBridge,
|
|
266
|
+
},
|
|
267
|
+
session: {
|
|
268
|
+
cwd,
|
|
269
|
+
agentDir: getAgentDir(),
|
|
270
|
+
modelRuntime: this.modelRuntime,
|
|
271
|
+
model: modelSpec.model as never,
|
|
272
|
+
thinkingLevel: modelSpec.thinkingLevel as never,
|
|
273
|
+
tools: agentDef?.tools,
|
|
274
|
+
customTools,
|
|
275
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
276
|
+
},
|
|
277
|
+
onError: (error) => this.options.onNotice?.(`${opts.label ?? "agent"}: extension error in ${error.event}: ${error.error}`),
|
|
214
278
|
});
|
|
215
279
|
|
|
216
|
-
const
|
|
217
|
-
|
|
280
|
+
const unhookAbort = whenAborted(signal, () => void session.abort());
|
|
281
|
+
// Forward tool calls for the viewer's Activity pane, and report a provider
|
|
282
|
+
// refusing the agent's model (the same check the subagent runner makes, so
|
|
283
|
+
// the refusal reaches every automatic picker — lib/model-unusable.ts).
|
|
284
|
+
// Never let a bad event shape kill the agent.
|
|
285
|
+
const unsubscribe = session.subscribe((event) => {
|
|
286
|
+
try {
|
|
287
|
+
if (event.type === "message_end") {
|
|
288
|
+
const reply = (event as { message?: { role?: string; stopReason?: string; errorMessage?: string; provider?: string; model?: string } }).message;
|
|
289
|
+
if (reply?.role === "assistant" && reply.stopReason === "error" && reply.errorMessage && reply.provider && reply.model && isModelUnavailableError(reply.errorMessage)) {
|
|
290
|
+
this.options.onModelUnusable?.(modelSpecOf({ provider: reply.provider, id: reply.model }), reply.errorMessage);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (event.type !== "tool_execution_start" || !onUpdate) return;
|
|
295
|
+
try {
|
|
296
|
+
const argsSummary = summarizeArgs((event as { args?: unknown }).args);
|
|
297
|
+
onUpdate({ tool: { name: event.toolName, argsSummary } });
|
|
298
|
+
} catch {
|
|
299
|
+
onUpdate({ tool: { name: event.toolName } });
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// observer only
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// Same prompt identity + cwd + model = same request prefix (see warmGate).
|
|
306
|
+
const model = session.model;
|
|
307
|
+
const releasePrefix = await this.warmGate.admitOnFirstToken(
|
|
308
|
+
prefixWarmKey(agentPromptIdentity(agentDef?.name), cwd, model ? modelSpecOf(model) : undefined),
|
|
309
|
+
session,
|
|
310
|
+
);
|
|
218
311
|
try {
|
|
219
312
|
await session.prompt(this.buildPrompt(prompt, Boolean(opts.schema)));
|
|
220
313
|
if (signal.aborted) throw new WorkflowScriptError("aborted");
|
|
@@ -245,12 +338,31 @@ export class AgentRunner {
|
|
|
245
338
|
worktreePath,
|
|
246
339
|
};
|
|
247
340
|
} finally {
|
|
248
|
-
|
|
341
|
+
// Whatever the outcome — success, failure, or an abort (a user stop, or
|
|
342
|
+
// the run finishing with this agent un-awaited) — the provider was
|
|
343
|
+
// billed for what ran, so the footer's all-in cost sees it.
|
|
344
|
+
const cost = session.getSessionStats().cost;
|
|
345
|
+
if (cost > 0) this.options.onUsage?.(cost);
|
|
346
|
+
releasePrefix(false);
|
|
347
|
+
unsubscribe?.();
|
|
348
|
+
unhookAbort();
|
|
249
349
|
session.dispose();
|
|
250
350
|
if (worktree) await cleanupWorktree(this.options.cwd, worktree);
|
|
251
351
|
}
|
|
252
352
|
}
|
|
253
353
|
|
|
354
|
+
private judgeDelegation(prompt: string, opts: AgentCallOptions, agentDef: AgentDefinition | undefined, signal: AbortSignal): Promise<void> {
|
|
355
|
+
return judgeWorkflowDelegation({
|
|
356
|
+
bridge: this.options.getPermissionBridge?.(),
|
|
357
|
+
liveMode: localGateMode(process.env[MODE_ENV], undefined),
|
|
358
|
+
prompt,
|
|
359
|
+
agentType: agentDef?.name,
|
|
360
|
+
label: opts.label,
|
|
361
|
+
cwd: this.options.cwd,
|
|
362
|
+
signal,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
254
366
|
private buildPrompt(prompt: string, structured: boolean): string {
|
|
255
367
|
if (!structured) {
|
|
256
368
|
return `${prompt}\n\nYour final message is returned verbatim to an orchestration script, not shown to a human — reply with the requested data/report only.`;
|
|
@@ -275,35 +387,10 @@ export class AgentRunner {
|
|
|
275
387
|
throw new Error("subagent never produced structured output");
|
|
276
388
|
}
|
|
277
389
|
|
|
278
|
-
/** Builds (once) and reuses the loader for this agentType, keyed by agent name. */
|
|
279
|
-
private loaderForAgentType(agentDef: AgentDefinition): Promise<DefaultResourceLoader> {
|
|
280
|
-
let pending = this.loadersByAgentType.get(agentDef.name);
|
|
281
|
-
if (!pending) {
|
|
282
|
-
pending = this.buildLoaderWithSystemPrompt(agentDef.systemPrompt);
|
|
283
|
-
// A failed build must not poison the cache — let the next call retry.
|
|
284
|
-
pending.catch(() => this.loadersByAgentType.delete(agentDef.name));
|
|
285
|
-
this.loadersByAgentType.set(agentDef.name, pending);
|
|
286
|
-
}
|
|
287
|
-
return pending;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private async buildLoaderWithSystemPrompt(systemPrompt: string): Promise<DefaultResourceLoader> {
|
|
291
|
-
const loader = new DefaultResourceLoader({
|
|
292
|
-
cwd: this.options.cwd,
|
|
293
|
-
agentDir: getAgentDir(),
|
|
294
|
-
noExtensions: true,
|
|
295
|
-
extensionFactories: [permissionGateFactory(this.options.cwd, os.homedir())],
|
|
296
|
-
systemPrompt,
|
|
297
|
-
});
|
|
298
|
-
await loader.reload();
|
|
299
|
-
return loader;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
390
|
dispose(): void {
|
|
303
|
-
// ModelRuntime
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
this.loadersByAgentType.clear();
|
|
391
|
+
// The ModelRuntime holds no OS resources that need explicit teardown today
|
|
392
|
+
// (each agent()'s loader dies with its session); this hook exists so
|
|
393
|
+
// run-manager can stay correct if that changes.
|
|
307
394
|
}
|
|
308
395
|
}
|
|
309
396
|
|
|
@@ -329,25 +416,8 @@ function buildStructuredOutputTool(schema: Record<string, unknown>, capture: { c
|
|
|
329
416
|
} as ToolDefinition;
|
|
330
417
|
}
|
|
331
418
|
|
|
332
|
-
export function finalAssistantText(messages: AgentMessage[]): string {
|
|
333
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
334
|
-
const message = messages[i];
|
|
335
|
-
if (message.role !== "assistant") continue;
|
|
336
|
-
const content = (message as { content: unknown }).content;
|
|
337
|
-
if (typeof content === "string") return content;
|
|
338
|
-
if (Array.isArray(content)) {
|
|
339
|
-
const text = content
|
|
340
|
-
.filter((b): b is { type: "text"; text: string } => (b as { type?: string }).type === "text")
|
|
341
|
-
.map((b) => b.text)
|
|
342
|
-
.join("");
|
|
343
|
-
if (text.trim()) return text;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return "";
|
|
347
|
-
}
|
|
348
|
-
|
|
349
419
|
/** Pull the first parseable JSON object/array out of free text (```json fences first). */
|
|
350
|
-
|
|
420
|
+
function extractJsonObject(text: string): unknown {
|
|
351
421
|
const fenced = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
352
422
|
const braceIndex = text.indexOf("{");
|
|
353
423
|
const bracketIndex = text.indexOf("[");
|