@bastani/atomic 0.8.25 → 0.8.26-alpha.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/index-heavy.ts +1754 -0
  4. package/dist/builtin/intercom/index.ts +374 -1746
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/result-renderers.ts +77 -0
  7. package/dist/builtin/mcp/CHANGELOG.md +10 -0
  8. package/dist/builtin/mcp/index.ts +151 -57
  9. package/dist/builtin/mcp/package.json +1 -1
  10. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  11. package/dist/builtin/subagents/package.json +1 -1
  12. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  13. package/dist/builtin/web-access/index-heavy.ts +2060 -0
  14. package/dist/builtin/web-access/index.ts +182 -2274
  15. package/dist/builtin/web-access/package.json +1 -1
  16. package/dist/builtin/web-access/result-renderers.ts +364 -0
  17. package/dist/builtin/workflows/CHANGELOG.md +9 -0
  18. package/dist/builtin/workflows/package.json +1 -1
  19. package/dist/builtin/workflows/src/extension/index.ts +13 -3
  20. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +53 -2
  21. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +12 -3
  22. package/dist/builtin/workflows/src/tui/inline-form-store.ts +17 -6
  23. package/dist/core/agent-session-services.d.ts.map +1 -1
  24. package/dist/core/agent-session-services.js +13 -0
  25. package/dist/core/agent-session-services.js.map +1 -1
  26. package/dist/core/extensions/loader.d.ts.map +1 -1
  27. package/dist/core/extensions/loader.js +7 -0
  28. package/dist/core/extensions/loader.js.map +1 -1
  29. package/dist/core/extensions/types.d.ts +13 -1
  30. package/dist/core/extensions/types.d.ts.map +1 -1
  31. package/dist/core/extensions/types.js.map +1 -1
  32. package/dist/core/resource-loader.d.ts.map +1 -1
  33. package/dist/core/resource-loader.js +17 -0
  34. package/dist/core/resource-loader.js.map +1 -1
  35. package/dist/core/timings.d.ts +9 -0
  36. package/dist/core/timings.d.ts.map +1 -1
  37. package/dist/core/timings.js +28 -1
  38. package/dist/core/timings.js.map +1 -1
  39. package/dist/main.d.ts.map +1 -1
  40. package/dist/main.js +4 -2
  41. package/dist/main.js.map +1 -1
  42. package/dist/modes/interactive/components/custom-message.d.ts +1 -0
  43. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  44. package/dist/modes/interactive/components/custom-message.js +36 -4
  45. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  46. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  47. package/dist/modes/interactive/interactive-mode.js +19 -7
  48. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  49. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.8.25",
3
+ "version": "0.8.26-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -0,0 +1,364 @@
1
+ import type { ToolDefinition } from "@bastani/atomic";
2
+ import { Box, Text } from "@mariozechner/pi-tui";
3
+ import { formatSeconds } from "./utils.js";
4
+
5
+ type ToolResultRenderer = NonNullable<ToolDefinition["renderResult"]>;
6
+ type ToolRenderResultArgs = Parameters<ToolResultRenderer>;
7
+ type ToolRenderResult = ReturnType<ToolResultRenderer>;
8
+ type RenderedResult = ToolRenderResultArgs[0];
9
+ type TextContentBlock = Extract<RenderedResult["content"][number], { type: "text" }>;
10
+
11
+ type QueryDetail = {
12
+ query: string;
13
+ provider: string | null;
14
+ answer: string | null;
15
+ sources: Array<{ title: string; url: string }>;
16
+ error: string | null;
17
+ };
18
+
19
+ type WebSearchResultDetails = {
20
+ queryCount?: number;
21
+ successfulQueries?: number;
22
+ totalResults?: number;
23
+ error?: string;
24
+ fetchId?: string;
25
+ fetchUrls?: string[];
26
+ phase?: string;
27
+ progress?: number;
28
+ currentQuery?: string;
29
+ curated?: boolean;
30
+ curatedFrom?: number;
31
+ curatedQueries?: QueryDetail[];
32
+ cancelled?: boolean;
33
+ cancelReason?: string;
34
+ summary?: {
35
+ text: string;
36
+ workflow: "summary-review";
37
+ model: string | null;
38
+ durationMs: number;
39
+ tokenEstimate: number;
40
+ fallbackUsed: boolean;
41
+ fallbackReason?: string;
42
+ edited?: boolean;
43
+ };
44
+ };
45
+
46
+ type CodeSearchResultDetails = {
47
+ query?: string;
48
+ maxTokens?: number;
49
+ error?: string;
50
+ };
51
+
52
+ type FetchContentResultDetails = {
53
+ urlCount?: number;
54
+ successful?: number;
55
+ totalChars?: number;
56
+ error?: string;
57
+ title?: string;
58
+ truncated?: boolean;
59
+ responseId?: string;
60
+ phase?: string;
61
+ progress?: number;
62
+ hasImage?: boolean;
63
+ imageCount?: number;
64
+ prompt?: string;
65
+ timestamp?: string;
66
+ frames?: number;
67
+ duration?: number;
68
+ };
69
+
70
+ type GetSearchContentResultDetails = {
71
+ error?: string;
72
+ query?: string;
73
+ url?: string;
74
+ title?: string;
75
+ resultCount?: number;
76
+ contentLength?: number;
77
+ };
78
+
79
+ function isTextContentBlock(block: RenderedResult["content"][number]): block is TextContentBlock {
80
+ return block.type === "text";
81
+ }
82
+
83
+ function firstTextContent(result: RenderedResult): string {
84
+ return result.content.find(isTextContentBlock)?.text ?? "";
85
+ }
86
+
87
+ function progressBar(progress: number): string {
88
+ const filled = Math.floor(progress * 10);
89
+ return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
90
+ }
91
+
92
+ export const renderWebSearchResult: ToolResultRenderer = (result, { expanded, isPartial }, theme) => {
93
+ const details = result.details as WebSearchResultDetails | undefined;
94
+
95
+ if (isPartial) {
96
+ if (details?.phase === "curating") {
97
+ return new Text(theme.fg("accent", "waiting for summary approval..."), 0, 0);
98
+ }
99
+ if (details?.phase === "searching") {
100
+ const progress = details?.progress ?? 0;
101
+ const bar = progressBar(progress);
102
+ const query = details?.currentQuery || "";
103
+ const display = query.length > 40 ? query.slice(0, 37) + "..." : query;
104
+ return new Text(theme.fg("accent", `[${bar}] ${display}`), 0, 0);
105
+ }
106
+ const progress = details?.progress ?? 0;
107
+ const bar = progressBar(progress);
108
+ return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "searching"}`), 0, 0);
109
+ }
110
+
111
+ if (details?.error) {
112
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
113
+ }
114
+
115
+ let statusLine: string;
116
+ const queryInfo = details?.queryCount === 1 ? "" : `${details?.successfulQueries}/${details?.queryCount} queries, `;
117
+ statusLine = theme.fg("success", `${queryInfo}${details?.totalResults ?? 0} sources`);
118
+ if (details?.curated && details?.curatedFrom) {
119
+ statusLine += theme.fg("muted", ` (${details.queryCount}/${details.curatedFrom} queries curated)`);
120
+ }
121
+ if (details?.fetchId && details?.fetchUrls) {
122
+ statusLine += theme.fg("muted", ` (fetching ${details.fetchUrls.length} URLs)`);
123
+ } else if (details?.fetchId) {
124
+ statusLine += theme.fg("muted", " (content ready)");
125
+ }
126
+
127
+ // Build expanded lines first so collapsed view can reference total count
128
+ const lines = [statusLine];
129
+ if (details?.summary?.text) {
130
+ lines.push("");
131
+ lines.push(theme.fg("accent", `── Summary (${details.summary.workflow}) ` + "─".repeat(32)));
132
+ lines.push("");
133
+ for (const line of details.summary.text.split("\n")) {
134
+ lines.push(` ${line}`);
135
+ }
136
+ lines.push("");
137
+ const metaParts = [
138
+ details.summary.model ? `model=${details.summary.model}` : "model=deterministic",
139
+ `duration=${details.summary.durationMs}ms`,
140
+ `tokens~${details.summary.tokenEstimate}`,
141
+ details.summary.fallbackUsed ? "fallback=true" : "fallback=false",
142
+ details.summary.edited ? "edited=true" : "edited=false",
143
+ ];
144
+ if (details.summary.fallbackReason) {
145
+ metaParts.push(`reason=${details.summary.fallbackReason}`);
146
+ }
147
+ lines.push(theme.fg("dim", " " + metaParts.join(" · ")));
148
+ }
149
+
150
+ const queryDetails = details?.curatedQueries;
151
+ if (queryDetails?.length) {
152
+ const kept = queryDetails.length;
153
+ const from = details?.curatedFrom ?? kept;
154
+ lines.push("");
155
+ lines.push(theme.fg("accent", `\u2500\u2500 Curated Results (${kept} of ${from} queries kept) ` + "\u2500".repeat(24)));
156
+
157
+ for (const cq of queryDetails) {
158
+ lines.push("");
159
+ const dq = cq.query.length > 65 ? cq.query.slice(0, 62) + "..." : cq.query;
160
+ const providerLabel = cq.provider ? ` (${cq.provider})` : "";
161
+ lines.push(theme.fg("accent", ` "${dq}"${providerLabel}`));
162
+
163
+ if (cq.error) {
164
+ lines.push(theme.fg("error", ` ${cq.error}`));
165
+ } else if (cq.answer) {
166
+ lines.push("");
167
+ for (const line of cq.answer.split("\n")) {
168
+ lines.push(` ${line}`);
169
+ }
170
+ }
171
+
172
+ if (cq.sources.length > 0) {
173
+ lines.push("");
174
+ for (const s of cq.sources) {
175
+ const domain = s.url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
176
+ const title = s.title.length > 50 ? s.title.slice(0, 47) + "..." : s.title;
177
+ lines.push(theme.fg("muted", ` \u25b8 ${title}`) + theme.fg("dim", ` \u00b7 ${domain}`));
178
+ }
179
+ }
180
+ }
181
+ lines.push("");
182
+ } else {
183
+ const textContent = firstTextContent(result);
184
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
185
+ for (const line of preview.split("\n")) {
186
+ lines.push(theme.fg("dim", line));
187
+ }
188
+ }
189
+
190
+ if (details?.fetchUrls && details.fetchUrls.length > 0) {
191
+ if (details.curated) {
192
+ lines.push(theme.fg("muted", `Fetching ${details.fetchUrls.length} URLs in background`));
193
+ } else {
194
+ lines.push(theme.fg("muted", "Fetching:"));
195
+ for (const u of details.fetchUrls.slice(0, 5)) {
196
+ const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
197
+ lines.push(theme.fg("dim", " " + display));
198
+ }
199
+ if (details.fetchUrls.length > 5) {
200
+ lines.push(theme.fg("dim", ` ... and ${details.fetchUrls.length - 5} more`));
201
+ }
202
+ }
203
+ }
204
+
205
+ const totalLines = lines.length;
206
+
207
+ if (!expanded) {
208
+ const box = new Box(1, 0, (t) => theme.bg("toolSuccessBg", t));
209
+ box.addChild(new Text(statusLine, 0, 0));
210
+
211
+ let collapsedLines = 1; // statusLine
212
+ const summaryPreview = details?.summary?.text?.trim() || "";
213
+ if (summaryPreview) {
214
+ const preview = summaryPreview.length > 120 ? summaryPreview.slice(0, 117) + "..." : summaryPreview;
215
+ box.addChild(new Text(theme.fg("dim", preview), 0, 0));
216
+ collapsedLines++;
217
+ } else if (details?.curatedQueries?.length) {
218
+ for (const cq of details.curatedQueries.slice(0, 3)) {
219
+ const dq = cq.query.length > 55 ? cq.query.slice(0, 52) + "..." : cq.query;
220
+ const srcCount = cq.sources?.length ?? 0;
221
+ const suffix = cq.error ? theme.fg("error", " (error)") : theme.fg("dim", ` · ${srcCount} sources`);
222
+ box.addChild(new Text(theme.fg("accent", ` "${dq}"`) + suffix, 0, 0));
223
+ collapsedLines++;
224
+ }
225
+ if (details.curatedQueries.length > 3) {
226
+ box.addChild(new Text(theme.fg("dim", ` ... and ${details.curatedQueries.length - 3} more`), 0, 0));
227
+ collapsedLines++;
228
+ }
229
+ } else {
230
+ const textContent = firstTextContent(result);
231
+ const firstContentLine = textContent.split("\n").find(l => {
232
+ const t = l.trim();
233
+ return t && !t.startsWith("[") && !t.startsWith("#") && !t.startsWith("---");
234
+ });
235
+ const fallbackLine = (firstContentLine?.trim() || "").replace(/\*\*/g, "");
236
+ if (fallbackLine) {
237
+ const preview = fallbackLine.length > 120 ? fallbackLine.slice(0, 117) + "..." : fallbackLine;
238
+ box.addChild(new Text(theme.fg("dim", preview), 0, 0));
239
+ collapsedLines++;
240
+ }
241
+ }
242
+ const moreLines = Math.max(0, totalLines - collapsedLines);
243
+ if (moreLines > 0) {
244
+ box.addChild(new Text(theme.fg("muted", `\n... (${moreLines} more lines, ${totalLines} total, CTRL+O Expand)`), 0, 0));
245
+ }
246
+ return box;
247
+ }
248
+
249
+ return new Text(lines.join("\n"), 0, 0);
250
+ };
251
+
252
+ export const renderCodeSearchResult: ToolResultRenderer = (result, { expanded }, theme) => {
253
+ const details = result.details as CodeSearchResultDetails | undefined;
254
+ if (details?.error) {
255
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
256
+ }
257
+
258
+ const summary = theme.fg("success", "code context returned") +
259
+ theme.fg("muted", ` (${details?.maxTokens ?? 5000} tokens max)`);
260
+ if (!expanded) return new Text(summary, 0, 0);
261
+
262
+ const textContent = firstTextContent(result);
263
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
264
+ return new Text(summary + "\n" + theme.fg("dim", preview), 0, 0);
265
+ };
266
+
267
+ export const renderFetchContentResult: ToolResultRenderer = (result, { expanded, isPartial }, theme) => {
268
+ const details = result.details as FetchContentResultDetails | undefined;
269
+
270
+ if (isPartial) {
271
+ const progress = details?.progress ?? 0;
272
+ const bar = progressBar(progress);
273
+ return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "fetching"}`), 0, 0);
274
+ }
275
+
276
+ if (details?.error) {
277
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
278
+ }
279
+
280
+ if (details?.urlCount === 1) {
281
+ const title = details?.title || "Untitled";
282
+ const imgCount = details?.imageCount ?? (details?.hasImage ? 1 : 0);
283
+ const imageBadge = imgCount > 1
284
+ ? theme.fg("accent", ` [${imgCount} images]`)
285
+ : imgCount === 1
286
+ ? theme.fg("accent", " [image]")
287
+ : "";
288
+ let statusLine = theme.fg("success", title) + theme.fg("muted", ` (${details?.totalChars ?? 0} chars)`) + imageBadge;
289
+ if (details?.truncated) {
290
+ statusLine += theme.fg("warning", " [truncated]");
291
+ }
292
+ if (typeof details?.duration === "number") {
293
+ statusLine += theme.fg("muted", ` | ${formatSeconds(Math.floor(details.duration))} total`);
294
+ }
295
+ const textContent = firstTextContent(result);
296
+ if (!expanded) {
297
+ const brief = textContent.length > 200 ? textContent.slice(0, 200) + "..." : textContent;
298
+ return new Text(statusLine + "\n" + theme.fg("dim", brief), 0, 0);
299
+ }
300
+ const lines = [statusLine];
301
+ if (details?.prompt) {
302
+ const display = details.prompt.length > 250 ? details.prompt.slice(0, 247) + "..." : details.prompt;
303
+ lines.push(theme.fg("dim", ` prompt: "${display}"`));
304
+ }
305
+ if (details?.timestamp) {
306
+ lines.push(theme.fg("dim", ` timestamp: ${details.timestamp}`));
307
+ }
308
+ if (typeof details?.frames === "number") {
309
+ lines.push(theme.fg("dim", ` frames: ${details.frames}`));
310
+ }
311
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
312
+ lines.push(theme.fg("dim", preview));
313
+ return new Text(lines.join("\n"), 0, 0);
314
+ }
315
+
316
+ const countColor = (details?.successful ?? 0) > 0 ? "success" : "error";
317
+ const statusLine = theme.fg(countColor, `${details?.successful}/${details?.urlCount} URLs`) + theme.fg("muted", " (content stored)");
318
+ if (!expanded) {
319
+ return new Text(statusLine, 0, 0);
320
+ }
321
+ const textContent = firstTextContent(result);
322
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
323
+ return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
324
+ };
325
+
326
+ export const renderGetSearchContentResult: ToolResultRenderer = (result, { expanded }, theme) => {
327
+ const details = result.details as GetSearchContentResultDetails | undefined;
328
+
329
+ if (details?.error) {
330
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
331
+ }
332
+
333
+ let statusLine: string;
334
+ if (details?.query) {
335
+ statusLine = theme.fg("success", `"${details.query}"`) + theme.fg("muted", ` (${details.resultCount} results)`);
336
+ } else {
337
+ statusLine = theme.fg("success", details?.title || "Content") + theme.fg("muted", ` (${details?.contentLength ?? 0} chars)`);
338
+ }
339
+
340
+ if (!expanded) {
341
+ return new Text(statusLine, 0, 0);
342
+ }
343
+
344
+ const textContent = firstTextContent(result);
345
+ const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
346
+ return new Text(statusLine + "\n" + theme.fg("dim", preview), 0, 0);
347
+ };
348
+
349
+ export function renderWebAccessToolResult(name: string, args: ToolRenderResultArgs): ToolRenderResult {
350
+ switch (name) {
351
+ case "web_search":
352
+ return renderWebSearchResult(...args);
353
+ case "code_search":
354
+ return renderCodeSearchResult(...args);
355
+ case "fetch_content":
356
+ return renderFetchContentResult(...args);
357
+ case "get_search_content":
358
+ return renderGetSearchContentResult(...args);
359
+ default: {
360
+ const theme = args[2];
361
+ return new Text(theme.fg("error", `Result renderer not found: ${name}`), 0, 0);
362
+ }
363
+ }
364
+ }
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.26-alpha.1] - 2026-06-05
10
+
11
+ ### Fixed
12
+
13
+ - Fixed the inline-form "snapshot lost" renderer and the `workflow.run.start`/`workflow.run.end` banner renderers returning bare strings, which crashed the host TUI with `child.render is not a function` when resuming a session containing persisted workflow custom messages. These renderers now return proper render components ([#1236](https://github.com/bastani-inc/atomic/issues/1236)).
14
+ - Fixed the workflow input form (the `/workflow <name>` argument selector) leaking into model context: spawning the picker and exiting without running the workflow no longer sends the form to the LLM. The input-form card is now emitted with `excludeFromContext` since it is transient UI, not conversation.
15
+ - Fixed the workflow input widget re-rendering in chat after `/resume`. Inline-form state is now cleared on `session_start`, and a rehydrated `workflows:input-form` card whose backing state is gone now renders nothing (returns `null`) instead of a stale form or "snapshot lost" placeholder.
16
+ - Stage sessions now emit `session_shutdown` before `dispose()` (mirroring the host `AgentSessionRuntime` teardown) so bound extensions receive a graceful shutdown signal instead of being silently invalidated. This stops disposed stage sessions from leaking child MCP servers and from triggering spurious stale-context "MCP initialization failed" errors when an extension's deferred `session_start` work races with stage disposal.
17
+
9
18
  ## [0.8.25] - 2026-06-04
10
19
 
11
20
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.8.25",
3
+ "version": "0.8.26-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -54,6 +54,7 @@ import {
54
54
  openInlineInputsForm,
55
55
  registerInlineFormRenderer,
56
56
  } from "../tui/inline-form-overlay.js";
57
+ import { clearForms } from "../tui/inline-form-store.js";
57
58
  import {
58
59
  registerChatSurfaceRenderer,
59
60
  emitChatSurface,
@@ -166,7 +167,7 @@ export interface PiMessageRenderOptions {
166
167
  expanded: boolean;
167
168
  }
168
169
 
169
- export type PiMessageRendererResult = string | PiMessageRenderComponent | undefined;
170
+ export type PiMessageRendererResult = string | PiMessageRenderComponent | null | undefined;
170
171
  export type PiMessageRenderer = (
171
172
  payload: unknown,
172
173
  options?: PiMessageRenderOptions,
@@ -3756,11 +3757,14 @@ function factory(pi: ExtensionAPI): void {
3756
3757
  // duplicating it into chat scroll just creates visual noise and pushes
3757
3758
  // older chat content out of view every time a stage transitions.
3758
3759
  if (typeof pi.registerMessageRenderer === "function") {
3760
+ // Wrap the string-producing banners in a render component: the host adds a
3761
+ // renderer's result directly as a TUI child, so a bare string would crash
3762
+ // `Container.render()` with "child.render is not a function".
3759
3763
  pi.registerMessageRenderer("workflow.run.start", (payload) =>
3760
- renderRunBanner(payload as RunStartPayload),
3764
+ dynamicTextRenderComponent(() => renderRunBanner(payload as RunStartPayload)),
3761
3765
  );
3762
3766
  pi.registerMessageRenderer("workflow.run.end", (payload) =>
3763
- renderRunSummary(payload as RunEndPayload),
3767
+ dynamicTextRenderComponent(() => renderRunSummary(payload as RunEndPayload)),
3764
3768
  );
3765
3769
  // Inline workflow-input form (Option C in the design conversation):
3766
3770
  // a sticky chat-history card driven by a custom EditorComponent. The
@@ -3834,6 +3838,12 @@ function factory(pi: ExtensionAPI): void {
3834
3838
  persistence: persistenceRef.current,
3835
3839
  });
3836
3840
  store.clear();
3841
+ // Drop any inline input-form state from a previous session in this pi
3842
+ // process. A resumed/replaced session must not render a stale live form,
3843
+ // and rehydrated `workflows:input-form` cards then resolve to no backing
3844
+ // state so their renderer suppresses output (input widget hidden after
3845
+ // /resume).
3846
+ clearForms();
3837
3847
  resetWorkflowLifecycleNotificationState(lifecycleNotificationState);
3838
3848
  resetWorkflowHilAnswerNotificationState(hilAnswerNotificationState);
3839
3849
  stageControlRegistry.clear();
@@ -285,6 +285,57 @@ function terminatingToolResultText(
285
285
  return undefined;
286
286
  }
287
287
 
288
+ /**
289
+ * A stage session backed by a real Atomic `AgentSession` exposes its
290
+ * `extensionRunner`. When workflow wiring binds extensions to a stage session it
291
+ * replays the `session_start` lifecycle (see wiring.ts `bindExtensions`), so
292
+ * extensions such as MCP begin per-session initialization. Tearing that session
293
+ * down with `dispose()` alone invalidates the extension runtime WITHOUT emitting
294
+ * `session_shutdown`, so those extensions never receive a graceful teardown
295
+ * signal: MCP, for example, logs a spurious stale-context "initialization
296
+ * failed" error when its deferred init races with disposal, and leaves any child
297
+ * MCP servers running.
298
+ *
299
+ * The test stub session (createTestAgentSession) has no `extensionRunner`, so the
300
+ * capability is optional and feature-detected at runtime.
301
+ */
302
+ type StageSessionExtensionRunner = {
303
+ hasHandlers(eventType: string): boolean;
304
+ emit(event: { readonly type: "session_shutdown"; readonly reason: "quit" }): Promise<unknown>;
305
+ };
306
+
307
+ function stageSessionExtensionRunner(
308
+ current: StageSessionRuntime,
309
+ ): StageSessionExtensionRunner | undefined {
310
+ const runner = (current as StageSessionRuntime & { extensionRunner?: StageSessionExtensionRunner })
311
+ .extensionRunner;
312
+ if (runner && typeof runner.hasHandlers === "function" && typeof runner.emit === "function") {
313
+ return runner;
314
+ }
315
+ return undefined;
316
+ }
317
+
318
+ /**
319
+ * Dispose a stage session, mirroring the host `AgentSessionRuntime` teardown:
320
+ * emit `session_shutdown` before `dispose()` whenever the session exposes a
321
+ * compatible extension runner, so extensions tear down per-session resources
322
+ * (and bump their lifecycle generation) instead of being silently invalidated.
323
+ * A throwing shutdown handler must never strand the session, so disposal always
324
+ * runs.
325
+ */
326
+ async function disposeStageSession(current: StageSessionRuntime | undefined): Promise<void> {
327
+ if (!current) return;
328
+ const runner = stageSessionExtensionRunner(current);
329
+ if (runner?.hasHandlers("session_shutdown")) {
330
+ try {
331
+ await runner.emit({ type: "session_shutdown", reason: "quit" });
332
+ } catch (error) {
333
+ console.error("atomic-workflows: stage session_shutdown handler failed", error);
334
+ }
335
+ }
336
+ await current.dispose();
337
+ }
338
+
288
339
  function asAgentSession(activeSession: StageSessionRuntime | undefined): AgentSession | undefined {
289
340
  if (!activeSession) return undefined;
290
341
  const candidate = activeSession as StageSessionRuntime & Partial<Pick<AgentSession, "state" | "sessionManager" | "modelRegistry" | "getContextUsage">>;
@@ -675,7 +726,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
675
726
  unsubscribeTerminateWatcher?.();
676
727
  unsubscribeTerminateWatcher = undefined;
677
728
  terminatingToolCallIds.clear();
678
- await current?.dispose();
729
+ await disposeStageSession(current);
679
730
  }
680
731
 
681
732
  async function promptWithPauseResume(
@@ -895,7 +946,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
895
946
  unsubscribeTerminateWatcher?.();
896
947
  unsubscribeTerminateWatcher = undefined;
897
948
  terminatingToolCallIds.clear();
898
- await session?.dispose();
949
+ await disposeStageSession(session);
899
950
  },
900
951
 
901
952
  __getLastAssistantText() {
@@ -79,7 +79,7 @@ interface CardComponent {
79
79
  invalidate?(): void;
80
80
  }
81
81
 
82
- type RawRenderer = (payload: unknown) => string | CardComponent | undefined;
82
+ type RawRenderer = (payload: unknown) => CardComponent | null | undefined;
83
83
 
84
84
  /**
85
85
  * Wire the message renderer once per live ExtensionAPI host. pi creates a new
@@ -106,8 +106,11 @@ export function registerInlineFormRenderer(pi: ExtensionAPI, theme: GraphTheme):
106
106
  if (!formId) return undefined;
107
107
  const state = getForm(formId);
108
108
  if (!state) {
109
- // Process restart / map evicted tombstone the entry.
110
- return ` ${message.content ?? "workflow form"} · (snapshot lost)`;
109
+ // No backing state the session was resumed/replaced (the store is
110
+ // cleared on session_start) or the map was evicted. Return null so the
111
+ // host renders nothing: the input widget must not reappear in chat after
112
+ // /resume rather than showing a stale or "snapshot lost" placeholder.
113
+ return null;
111
114
  }
112
115
  return {
113
116
  // The card is fully reactive: read fresh state on every render call,
@@ -291,11 +294,17 @@ export async function openInlineInputsForm(
291
294
  display?: boolean;
292
295
  details?: FormMessageDetails;
293
296
  },
297
+ options?: { excludeFromContext?: boolean },
294
298
  ) => void).call(pi, {
295
299
  customType: CUSTOM_TYPE,
296
300
  content: opts.workflowName,
297
301
  display: true,
298
302
  details: { formId },
303
+ }, {
304
+ // The input form is a transient UI surface, not conversation. Keep it
305
+ // out of LLM context so spawning the picker and exiting without
306
+ // running the workflow never leaks the form into the model.
307
+ excludeFromContext: true,
299
308
  });
300
309
  } catch {
301
310
  activeEditor?.dispose?.();
@@ -12,11 +12,12 @@
12
12
  * `finalizeForm(id, "submit")` → status = "submitted", values frozen
13
13
  * `finalizeForm(id, "cancel")` → status = "cancelled"
14
14
  *
15
- * After finalize the state stays in the map forever (module-lifetime). The
16
- * renderer reads it to display the historical card. If the process restarts,
17
- * the map is empty and the renderer falls back to a "form (snapshot lost)"
18
- * placeholder acceptable because frozen cards are decorative, not
19
- * functional.
15
+ * After finalize the state stays in the map for the lifetime of the session.
16
+ * The renderer reads it to display the historical card. On a session boundary
17
+ * (`session_start`: new/resume/fork/reload) the store is cleared via
18
+ * {@link clearForms}, so a rehydrated `workflows:input-form` message has no
19
+ * backing state and its renderer suppresses output (returns null) — the input
20
+ * widget never reappears in chat after `/resume`.
20
21
  *
21
22
  * Why a global registry instead of closure capture: the message renderer is
22
23
  * registered ONCE at factory time and called many times for any number of
@@ -73,7 +74,17 @@ export function finalizeForm(formId: string, outcome: "submit" | "cancel"): void
73
74
  touch(s);
74
75
  }
75
76
 
77
+ /**
78
+ * Clear all inline form state. Called on `session_start` so a resumed or
79
+ * replaced session never renders a stale live form, and so a rehydrated
80
+ * `workflows:input-form` message resolves to no backing state (its renderer
81
+ * then returns null and the host renders nothing).
82
+ */
83
+ export function clearForms(): void {
84
+ FORMS.clear();
85
+ }
86
+
76
87
  /** Test helper — clear the registry between tests. */
77
88
  export function _resetForms(): void {
78
- FORMS.clear();
89
+ clearForms();
79
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-session-services.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-services.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAyB,KAAK,4BAA4B,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACrH,OAAO,EAAE,KAAK,yBAAyB,EAAE,KAAK,wBAAwB,EAAsB,MAAM,UAAU,CAAC;AAC7G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,WAAW,6BAA6B;IAC7C,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iCAAiC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;IACpD,qBAAqB,CAAC,EAAE,IAAI,CAAC,4BAA4B,EAAE,KAAK,GAAG,UAAU,GAAG,iBAAiB,CAAC,CAAC;CACnG;AAED;;;;;GAKG;AACH,MAAM,WAAW,qCAAqC;IACrD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IAC3E,KAAK,CAAC,EAAE,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAC3C,aAAa,CAAC,EAAE,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAC3D,OAAO,CAAC,EAAE,yBAAyB,CAAC,SAAS,CAAC,CAAC;IAC/C,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,cAAc,EAAE,cAAc,CAAC;IAC/B,WAAW,EAAE,6BAA6B,EAAE,CAAC;CAC7C;AAkDD;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC/C,OAAO,EAAE,iCAAiC,GACxC,OAAO,CAAC,oBAAoB,CAAC,CAuC/B;AAED;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CACnD,OAAO,EAAE,qCAAqC,GAC5C,OAAO,CAAC,wBAAwB,CAAC,CAkBnC","sourcesContent":["import { join } from \"node:path\";\nimport type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport type { Api, Model } from \"@earendil-works/pi-ai\";\nimport { getAgentDir } from \"../config.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport { AuthStorage } from \"./auth-storage.ts\";\nimport type { SessionStartEvent, ToolDefinition } from \"./extensions/index.ts\";\nimport { ModelRegistry } from \"./model-registry.ts\";\nimport { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from \"./resource-loader.ts\";\nimport { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } from \"./sdk.ts\";\nimport type { SessionManager } from \"./session-manager.ts\";\nimport { SettingsManager } from \"./settings-manager.ts\";\n\n/**\n * Non-fatal issues collected while creating services or sessions.\n *\n * Runtime creation returns diagnostics to the caller instead of printing or\n * exiting. The app layer decides whether warnings should be shown and whether\n * errors should abort startup.\n */\nexport interface AgentSessionRuntimeDiagnostic {\n\ttype: \"info\" | \"warning\" | \"error\";\n\tmessage: string;\n}\n\n/**\n * Inputs for creating cwd-bound runtime services.\n *\n * These services are recreated whenever the effective session cwd changes.\n * CLI-provided resource paths should be resolved to absolute paths before they\n * reach this function, so later cwd switches do not reinterpret them.\n */\nexport interface CreateAgentSessionServicesOptions {\n\tcwd: string;\n\tagentDir?: string;\n\tauthStorage?: AuthStorage;\n\tsettingsManager?: SettingsManager;\n\tmodelRegistry?: ModelRegistry;\n\textensionFlagValues?: Map<string, boolean | string>;\n\tresourceLoaderOptions?: Omit<DefaultResourceLoaderOptions, \"cwd\" | \"agentDir\" | \"settingsManager\">;\n}\n\n/**\n * Inputs for creating an AgentSession from already-created services.\n *\n * Use this after services exist and any cwd-bound model/tool/session options\n * have been resolved against those services.\n */\nexport interface CreateAgentSessionFromServicesOptions {\n\tservices: AgentSessionServices;\n\tsessionManager: SessionManager;\n\tsessionStartEvent?: SessionStartEvent;\n\tmodel?: Model<Api>;\n\tthinkingLevel?: ThinkingLevel;\n\tscopedModels?: Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }>;\n\ttools?: CreateAgentSessionOptions[\"tools\"];\n\texcludedTools?: CreateAgentSessionOptions[\"excludedTools\"];\n\tnoTools?: CreateAgentSessionOptions[\"noTools\"];\n\tcustomTools?: ToolDefinition[];\n}\n\n/**\n * Coherent cwd-bound runtime services for one effective session cwd.\n *\n * This is infrastructure only. The AgentSession itself is created separately so\n * session options can be resolved against these services first.\n */\nexport interface AgentSessionServices {\n\tcwd: string;\n\tagentDir: string;\n\tauthStorage: AuthStorage;\n\tsettingsManager: SettingsManager;\n\tmodelRegistry: ModelRegistry;\n\tresourceLoader: ResourceLoader;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n}\n\nfunction applyExtensionFlagValues(\n\tresourceLoader: ResourceLoader,\n\textensionFlagValues: Map<string, boolean | string> | undefined,\n): AgentSessionRuntimeDiagnostic[] {\n\tif (!extensionFlagValues) {\n\t\treturn [];\n\t}\n\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tconst extensionsResult = resourceLoader.getExtensions();\n\tconst registeredFlags = new Map<string, { type: \"boolean\" | \"string\" }>();\n\tfor (const extension of extensionsResult.extensions) {\n\t\tfor (const [name, flag] of extension.flags) {\n\t\t\tregisteredFlags.set(name, { type: flag.type });\n\t\t}\n\t}\n\n\tconst unknownFlags: string[] = [];\n\tfor (const [name, value] of extensionFlagValues) {\n\t\tconst flag = registeredFlags.get(name);\n\t\tif (!flag) {\n\t\t\tunknownFlags.push(name);\n\t\t\tcontinue;\n\t\t}\n\t\tif (flag.type === \"boolean\") {\n\t\t\textensionsResult.runtime.flagValues.set(name, true);\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof value === \"string\") {\n\t\t\textensionsResult.runtime.flagValues.set(name, value);\n\t\t\tcontinue;\n\t\t}\n\t\tdiagnostics.push({\n\t\t\ttype: \"error\",\n\t\t\tmessage: `Extension flag \"--${name}\" requires a value`,\n\t\t});\n\t}\n\n\tif (unknownFlags.length > 0) {\n\t\tdiagnostics.push({\n\t\t\ttype: \"error\",\n\t\t\tmessage: `Unknown option${unknownFlags.length === 1 ? \"\" : \"s\"}: ${unknownFlags.map((name) => `--${name}`).join(\", \")}`,\n\t\t});\n\t}\n\n\treturn diagnostics;\n}\n\n/**\n * Create cwd-bound runtime services.\n *\n * Returns services plus diagnostics. It does not create an AgentSession.\n */\nexport async function createAgentSessionServices(\n\toptions: CreateAgentSessionServicesOptions,\n): Promise<AgentSessionServices> {\n\tconst cwd = resolvePath(options.cwd);\n\tconst agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();\n\tconst authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, \"auth.json\"));\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, \"models.json\"));\n\tconst resourceLoader = new DefaultResourceLoader({\n\t\t...(options.resourceLoaderOptions ?? {}),\n\t\tcwd,\n\t\tagentDir,\n\t\tsettingsManager,\n\t});\n\tawait resourceLoader.reload();\n\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tconst extensionsResult = resourceLoader.getExtensions();\n\tfor (const { name, config, extensionPath } of extensionsResult.runtime.pendingProviderRegistrations) {\n\t\ttry {\n\t\t\tmodelRegistry.registerProvider(name, config);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tdiagnostics.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Extension \"${extensionPath}\" error: ${message}`,\n\t\t\t});\n\t\t}\n\t}\n\textensionsResult.runtime.pendingProviderRegistrations = [];\n\tdiagnostics.push(...applyExtensionFlagValues(resourceLoader, options.extensionFlagValues));\n\n\treturn {\n\t\tcwd,\n\t\tagentDir,\n\t\tauthStorage,\n\t\tsettingsManager,\n\t\tmodelRegistry,\n\t\tresourceLoader,\n\t\tdiagnostics,\n\t};\n}\n\n/**\n * Create an AgentSession from previously created services.\n *\n * This keeps session creation separate from service creation so callers can\n * resolve model, thinking, tools, and other session inputs against the target\n * cwd before constructing the session.\n */\nexport async function createAgentSessionFromServices(\n\toptions: CreateAgentSessionFromServicesOptions,\n): Promise<CreateAgentSessionResult> {\n\treturn createAgentSession({\n\t\tcwd: options.services.cwd,\n\t\tagentDir: options.services.agentDir,\n\t\tauthStorage: options.services.authStorage,\n\t\tsettingsManager: options.services.settingsManager,\n\t\tmodelRegistry: options.services.modelRegistry,\n\t\tresourceLoader: options.services.resourceLoader,\n\t\tsessionManager: options.sessionManager,\n\t\tmodel: options.model,\n\t\tthinkingLevel: options.thinkingLevel,\n\t\tscopedModels: options.scopedModels,\n\t\ttools: options.tools,\n\t\texcludedTools: options.excludedTools,\n\t\tnoTools: options.noTools,\n\t\tcustomTools: options.customTools,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n}\n"]}
1
+ {"version":3,"file":"agent-session-services.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-services.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAyB,KAAK,4BAA4B,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACrH,OAAO,EAAE,KAAK,yBAAyB,EAAE,KAAK,wBAAwB,EAAsB,MAAM,UAAU,CAAC;AAC7G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD;;;;;;GAMG;AACH,MAAM,WAAW,6BAA6B;IAC7C,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iCAAiC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;IACpD,qBAAqB,CAAC,EAAE,IAAI,CAAC,4BAA4B,EAAE,KAAK,GAAG,UAAU,GAAG,iBAAiB,CAAC,CAAC;CACnG;AAED;;;;;GAKG;AACH,MAAM,WAAW,qCAAqC;IACrD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IAC3E,KAAK,CAAC,EAAE,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAC3C,aAAa,CAAC,EAAE,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAC3D,OAAO,CAAC,EAAE,yBAAyB,CAAC,SAAS,CAAC,CAAC;IAC/C,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,cAAc,EAAE,cAAc,CAAC;IAC/B,WAAW,EAAE,6BAA6B,EAAE,CAAC;CAC7C;AAkDD;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC/C,OAAO,EAAE,iCAAiC,GACxC,OAAO,CAAC,oBAAoB,CAAC,CAmD/B;AAED;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CACnD,OAAO,EAAE,qCAAqC,GAC5C,OAAO,CAAC,wBAAwB,CAAC,CAkBnC","sourcesContent":["import { join } from \"node:path\";\nimport type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport type { Api, Model } from \"@earendil-works/pi-ai\";\nimport { getAgentDir } from \"../config.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport { AuthStorage } from \"./auth-storage.ts\";\nimport type { SessionStartEvent, ToolDefinition } from \"./extensions/index.ts\";\nimport { ModelRegistry } from \"./model-registry.ts\";\nimport { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from \"./resource-loader.ts\";\nimport { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } from \"./sdk.ts\";\nimport type { SessionManager } from \"./session-manager.ts\";\nimport { SettingsManager } from \"./settings-manager.ts\";\nimport { endTimingSpan, startTimingSpan } from \"./timings.ts\";\n\n/**\n * Non-fatal issues collected while creating services or sessions.\n *\n * Runtime creation returns diagnostics to the caller instead of printing or\n * exiting. The app layer decides whether warnings should be shown and whether\n * errors should abort startup.\n */\nexport interface AgentSessionRuntimeDiagnostic {\n\ttype: \"info\" | \"warning\" | \"error\";\n\tmessage: string;\n}\n\n/**\n * Inputs for creating cwd-bound runtime services.\n *\n * These services are recreated whenever the effective session cwd changes.\n * CLI-provided resource paths should be resolved to absolute paths before they\n * reach this function, so later cwd switches do not reinterpret them.\n */\nexport interface CreateAgentSessionServicesOptions {\n\tcwd: string;\n\tagentDir?: string;\n\tauthStorage?: AuthStorage;\n\tsettingsManager?: SettingsManager;\n\tmodelRegistry?: ModelRegistry;\n\textensionFlagValues?: Map<string, boolean | string>;\n\tresourceLoaderOptions?: Omit<DefaultResourceLoaderOptions, \"cwd\" | \"agentDir\" | \"settingsManager\">;\n}\n\n/**\n * Inputs for creating an AgentSession from already-created services.\n *\n * Use this after services exist and any cwd-bound model/tool/session options\n * have been resolved against those services.\n */\nexport interface CreateAgentSessionFromServicesOptions {\n\tservices: AgentSessionServices;\n\tsessionManager: SessionManager;\n\tsessionStartEvent?: SessionStartEvent;\n\tmodel?: Model<Api>;\n\tthinkingLevel?: ThinkingLevel;\n\tscopedModels?: Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }>;\n\ttools?: CreateAgentSessionOptions[\"tools\"];\n\texcludedTools?: CreateAgentSessionOptions[\"excludedTools\"];\n\tnoTools?: CreateAgentSessionOptions[\"noTools\"];\n\tcustomTools?: ToolDefinition[];\n}\n\n/**\n * Coherent cwd-bound runtime services for one effective session cwd.\n *\n * This is infrastructure only. The AgentSession itself is created separately so\n * session options can be resolved against these services first.\n */\nexport interface AgentSessionServices {\n\tcwd: string;\n\tagentDir: string;\n\tauthStorage: AuthStorage;\n\tsettingsManager: SettingsManager;\n\tmodelRegistry: ModelRegistry;\n\tresourceLoader: ResourceLoader;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n}\n\nfunction applyExtensionFlagValues(\n\tresourceLoader: ResourceLoader,\n\textensionFlagValues: Map<string, boolean | string> | undefined,\n): AgentSessionRuntimeDiagnostic[] {\n\tif (!extensionFlagValues) {\n\t\treturn [];\n\t}\n\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tconst extensionsResult = resourceLoader.getExtensions();\n\tconst registeredFlags = new Map<string, { type: \"boolean\" | \"string\" }>();\n\tfor (const extension of extensionsResult.extensions) {\n\t\tfor (const [name, flag] of extension.flags) {\n\t\t\tregisteredFlags.set(name, { type: flag.type });\n\t\t}\n\t}\n\n\tconst unknownFlags: string[] = [];\n\tfor (const [name, value] of extensionFlagValues) {\n\t\tconst flag = registeredFlags.get(name);\n\t\tif (!flag) {\n\t\t\tunknownFlags.push(name);\n\t\t\tcontinue;\n\t\t}\n\t\tif (flag.type === \"boolean\") {\n\t\t\textensionsResult.runtime.flagValues.set(name, true);\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof value === \"string\") {\n\t\t\textensionsResult.runtime.flagValues.set(name, value);\n\t\t\tcontinue;\n\t\t}\n\t\tdiagnostics.push({\n\t\t\ttype: \"error\",\n\t\t\tmessage: `Extension flag \"--${name}\" requires a value`,\n\t\t});\n\t}\n\n\tif (unknownFlags.length > 0) {\n\t\tdiagnostics.push({\n\t\t\ttype: \"error\",\n\t\t\tmessage: `Unknown option${unknownFlags.length === 1 ? \"\" : \"s\"}: ${unknownFlags.map((name) => `--${name}`).join(\", \")}`,\n\t\t});\n\t}\n\n\treturn diagnostics;\n}\n\n/**\n * Create cwd-bound runtime services.\n *\n * Returns services plus diagnostics. It does not create an AgentSession.\n */\nexport async function createAgentSessionServices(\n\toptions: CreateAgentSessionServicesOptions,\n): Promise<AgentSessionServices> {\n\tconst cwd = resolvePath(options.cwd);\n\tconst agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();\n\tconst authStorageSpan = startTimingSpan(\"createAgentSessionServices.authStorage\");\n\tconst authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, \"auth.json\"));\n\tendTimingSpan(authStorageSpan);\n\tconst settingsSpan = startTimingSpan(\"createAgentSessionServices.settingsManager\");\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tendTimingSpan(settingsSpan);\n\tconst modelRegistrySpan = startTimingSpan(\"createAgentSessionServices.modelRegistry\");\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, \"models.json\"));\n\tendTimingSpan(modelRegistrySpan);\n\tconst resourceLoader = new DefaultResourceLoader({\n\t\t...(options.resourceLoaderOptions ?? {}),\n\t\tcwd,\n\t\tagentDir,\n\t\tsettingsManager,\n\t});\n\tconst reloadSpan = startTimingSpan(\"createAgentSessionServices.resourceLoader.reload\");\n\tawait resourceLoader.reload();\n\tendTimingSpan(reloadSpan);\n\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tconst providerSpan = startTimingSpan(\"createAgentSessionServices.providerRegistrations\");\n\tconst extensionsResult = resourceLoader.getExtensions();\n\tfor (const { name, config, extensionPath } of extensionsResult.runtime.pendingProviderRegistrations) {\n\t\ttry {\n\t\t\tmodelRegistry.registerProvider(name, config);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tdiagnostics.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Extension \"${extensionPath}\" error: ${message}`,\n\t\t\t});\n\t\t}\n\t}\n\textensionsResult.runtime.pendingProviderRegistrations = [];\n\tendTimingSpan(providerSpan);\n\tconst flagSpan = startTimingSpan(\"createAgentSessionServices.extensionFlagValidation\");\n\tdiagnostics.push(...applyExtensionFlagValues(resourceLoader, options.extensionFlagValues));\n\tendTimingSpan(flagSpan);\n\n\treturn {\n\t\tcwd,\n\t\tagentDir,\n\t\tauthStorage,\n\t\tsettingsManager,\n\t\tmodelRegistry,\n\t\tresourceLoader,\n\t\tdiagnostics,\n\t};\n}\n\n/**\n * Create an AgentSession from previously created services.\n *\n * This keeps session creation separate from service creation so callers can\n * resolve model, thinking, tools, and other session inputs against the target\n * cwd before constructing the session.\n */\nexport async function createAgentSessionFromServices(\n\toptions: CreateAgentSessionFromServicesOptions,\n): Promise<CreateAgentSessionResult> {\n\treturn createAgentSession({\n\t\tcwd: options.services.cwd,\n\t\tagentDir: options.services.agentDir,\n\t\tauthStorage: options.services.authStorage,\n\t\tsettingsManager: options.services.settingsManager,\n\t\tmodelRegistry: options.services.modelRegistry,\n\t\tresourceLoader: options.services.resourceLoader,\n\t\tsessionManager: options.sessionManager,\n\t\tmodel: options.model,\n\t\tthinkingLevel: options.thinkingLevel,\n\t\tscopedModels: options.scopedModels,\n\t\ttools: options.tools,\n\t\texcludedTools: options.excludedTools,\n\t\tnoTools: options.noTools,\n\t\tcustomTools: options.customTools,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n}\n"]}