@visual-z/xeno 0.2.1 → 0.4.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.
Files changed (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,11 +1,15 @@
1
1
  /** Pi's factory/registration protocol, attached to xeno's session rather than another AgentSession. */
2
2
  import { loadExtensionFactory } from "./extension-loader.ts";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
3
4
  import type { ResourceIndex } from "../store/resource-index.ts";
4
5
  import type { ExtensionEvent, LogEvent } from "../core/events.ts";
5
6
  import { isJsonValue, type JsonValue } from "../core/tool-contract.ts";
6
- import { runTool, type ToolContext, type ToolDefinition } from "./tools.ts";
7
+ import type { ToolContext, ToolDefinition } from "./tools.ts";
8
+ import { nativeCommandResult } from "./tools.ts";
7
9
  import { attachImageBytes } from "./images.ts";
8
10
  import type { ExtensionComponent, ExtensionUI, ExtensionTheme } from "./extension-ui.ts";
11
+ import { DISCOVERY_TOOL, DISCOVERY_TOOL_NAME } from "./tool-discovery.ts";
12
+ import { LazyIntegration, type ExtensionIntegration } from "./integrations.ts";
9
13
 
10
14
  export interface ExtensionContext {
11
15
  ui: ExtensionUI; cwd: string; mode: "tui" | "json" | "print"; hasUI: boolean;
@@ -35,6 +39,8 @@ export interface ExtensionToolRenderContext {
35
39
  }
36
40
  export interface ExtensionTool {
37
41
  name: string; label?: string; description: string; parameters: unknown;
42
+ /** R11: omitted remains direct; deferred schemas are activated at a Session checkpoint. */
43
+ exposure?: "direct" | "deferred";
38
44
  promptSnippet?: string; promptGuidelines?: string[]; executionMode?: "parallel" | "sequential";
39
45
  prepareArguments?(input: unknown): unknown;
40
46
  execute(id: string, args: Record<string, unknown>, signal: AbortSignal | undefined,
@@ -52,6 +58,7 @@ export interface ExtensionActions {
52
58
  setLabel(id: string, label: string | undefined): Promise<void>;
53
59
  setTools(names: string[]): Promise<void>;
54
60
  getTools(): { name: string; description: string; parameters: unknown }[];
61
+ getAllTools?(): { name: string; description: string; parameters: unknown }[];
55
62
  setModel(model: unknown): Promise<boolean>;
56
63
  setThinking(level: string): Promise<void>;
57
64
  exec(command: string, args: string[], options?: { cwd?: string; timeout?: number; signal?: AbortSignal }): Promise<{ stdout: string; stderr: string; code: number; killed: boolean }>;
@@ -60,6 +67,7 @@ export interface ExtensionAPI {
60
67
  on(event: string, handler: ExtensionHandler): () => void;
61
68
  registerCommand(name: string, command: ExtensionCommand): void;
62
69
  registerTool(tool: ExtensionTool): void;
70
+ registerIntegration(integration: ExtensionIntegration): void;
63
71
  registerShortcut(key: string, shortcut: { description?: string; handler(ctx: ExtensionContext): void | Promise<void> }): void;
64
72
  registerFlag(name: string, options: Flag): void;
65
73
  getFlag(name: string): string | boolean | undefined;
@@ -106,11 +114,16 @@ export class ExtensionRegistry {
106
114
  private handlers = new Map<string, Set<ExtensionHandler>>();
107
115
  private bus = new Map<string, Set<(data: unknown) => void>>();
108
116
  private pending = new Set<Promise<void>>();
117
+ /** Pi A / D8: void session mutations still have a durable invocation order. */
118
+ private actionTail: Promise<void> = Promise.resolve();
109
119
  private disposers: (() => void)[] = [];
110
120
  private detachListeners = new Set<() => void>();
111
121
  private toolUpdateListeners = new Set<(name: string, id: string, input: unknown, result: ExtensionToolResult) => void>();
112
122
  private actions: ExtensionActions | undefined;
123
+ private readonly execution = new AsyncLocalStorage<ToolContext>();
113
124
  private disposed = false;
125
+ private readonly integrations = new Map<string, LazyIntegration>();
126
+ private readonly integrationTools = new Map<string, LazyIntegration>();
114
127
  private readonly options: ExtensionRegistryOptions;
115
128
  constructor(options: ExtensionRegistryOptions = {}) { this.options = options; }
116
129
 
@@ -124,8 +137,10 @@ export class ExtensionRegistry {
124
137
  return this.actions;
125
138
  }
126
139
  private enqueue(operation: () => Promise<void>): void {
127
- const pending = Promise.resolve().then(operation).catch(error => this.report(String(error))).finally(() => this.pending.delete(pending));
140
+ const pending = this.actionTail.then(operation).catch(error => this.report(String(error))).finally(() => this.pending.delete(pending));
128
141
  this.pending.add(pending);
142
+ // A failing observer must not poison later accepted work or create an unhandled rejection.
143
+ this.actionTail = pending.catch(() => {});
129
144
  }
130
145
  async settle(): Promise<void> { while (this.pending.size) await Promise.all([...this.pending]); }
131
146
  private report(message: string): void { this.diagnostics.push(message); this.options.report?.(`Extension: ${message}`); }
@@ -146,9 +161,42 @@ export class ExtensionRegistry {
146
161
  },
147
162
  registerTool: tool => {
148
163
  if (!/^[a-zA-Z0-9_-]+$/.test(tool.name) || typeof tool.execute !== "function") throw new Error("Invalid tool definition");
149
- if (["note", "history", "branch", "resolve"].includes(tool.name)) throw new Error(`${tool.name} owns the append-only harness and cannot be replaced`);
164
+ if (["note", "history", "branch", "resolve", DISCOVERY_TOOL_NAME].includes(tool.name)) throw new Error(`${tool.name} owns the append-only harness and cannot be replaced`);
165
+ if (tool.exposure !== undefined && tool.exposure !== "direct" && tool.exposure !== "deferred") throw new Error("Unknown tool exposure");
166
+ if (tool.exposure === "deferred" && tool.name.length > 128) throw new Error("Deferred tool names must fit the discovery receipt");
150
167
  extensionSchema(tool.parameters); this.insert(this.tools, tool.name, tool, source);
151
168
  },
169
+ registerIntegration: integration => {
170
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(integration.name) || this.integrations.has(integration.name)
171
+ || typeof integration.connect !== "function" || !Array.isArray(integration.tools) || !integration.tools.length) throw Error("Invalid or duplicate integration");
172
+ const repair = integration.repair;
173
+ if (repair && (!/^[a-zA-Z0-9_-]{1,64}$/.test(repair.command) || this.commands.has(repair.command)
174
+ || this.options.reserved?.includes(repair.command) || typeof repair.run !== "function")) throw Error("Invalid or duplicate integration repair command");
175
+ const names = new Set<string>();
176
+ for (const tool of integration.tools) {
177
+ if (names.has(tool.name) || this.tools.has(tool.name)) throw Error("Duplicate integration tool");
178
+ names.add(tool.name);
179
+ }
180
+ // Copy the manifest before publishing. Registrations and live connection ownership share one generation.
181
+ const owner = new LazyIntegration({ ...integration, ...(repair ? { repair: { ...repair } } : {}),
182
+ tools: integration.tools.map(tool => ({ ...tool, parameters: structuredClone(tool.parameters) })) });
183
+ const added: string[] = [];
184
+ try {
185
+ for (const descriptor of owner.definition.tools) {
186
+ this.api(source).registerTool({ ...descriptor, exposure: descriptor.exposure ?? "deferred", execute: async () => { throw Error("Integration tools require their owner"); } });
187
+ added.push(descriptor.name); this.integrationTools.set(descriptor.name, owner);
188
+ }
189
+ if (repair) this.api(source).registerCommand(repair.command, { description: repair.description, handler: async (_args, ctx) => {
190
+ const result = await owner.repair(ctx);
191
+ await this.active().append({ customType: "integration_repair", content: result.content, display: true, inContext: false,
192
+ ...(result.data === undefined ? {} : { data: result.data }) });
193
+ } });
194
+ this.integrations.set(integration.name, owner);
195
+ } catch (error) {
196
+ for (const name of added) { this.tools.delete(name); this.integrationTools.delete(name); }
197
+ void owner.dispose(); throw error;
198
+ }
199
+ },
152
200
  registerShortcut: (name, value) => this.insert(this.shortcuts, name, value, source),
153
201
  registerFlag: (name, value) => {
154
202
  if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(name) || this.options.reservedFlags?.includes(name)) throw new Error(`CLI flag --${name} is reserved or invalid`);
@@ -166,13 +214,27 @@ export class ExtensionRegistry {
166
214
  setSessionName: name => this.enqueue(() => this.active().setName(name)),
167
215
  setLabel: (id, label) => this.enqueue(() => this.active().setLabel(id, label)),
168
216
  getActiveTools: () => this.active().getTools().map(tool => tool.name),
169
- getAllTools: () => this.active().getTools(),
217
+ getAllTools: () => this.active().getAllTools?.() ?? this.active().getTools(),
170
218
  setActiveTools: names => this.enqueue(() => this.active().setTools(names)),
171
219
  setModel: model => this.active().setModel(model),
172
220
  getThinkingLevel: () => this.active().context().thinkingLevel,
173
221
  setThinkingLevel: level => this.enqueue(() => this.active().setThinking(level)),
174
222
  getCommands: () => [...this.commands].map(([name, command]) => ({ name, ...(command.description ? { description: command.description } : {}) })),
175
- exec: (command, args, options) => this.active().exec(command, args, options),
223
+ exec: (command, args, options) => {
224
+ const pending = (async () => {
225
+ this.active();
226
+ const scope = this.execution.getStore();
227
+ if (!scope) return this.active().exec(command, [...args], options);
228
+ if (!scope.invokeHost || !scope.outputStore) throw new Error("native commands require a live Session host scope");
229
+ const result = await scope.invokeHost({ argv: [command, ...args], ...(options?.cwd !== undefined ? { cwd: options.cwd } : {}),
230
+ ...(options?.timeout !== undefined ? { timeout: options.timeout } : {}) }, options?.signal);
231
+ return nativeCommandResult(result, scope.outputStore);
232
+ })();
233
+ // The host owns accepted work even if the extension drops this SDK promise.
234
+ // Keep the rejection available to an awaiter while observing it ourselves.
235
+ void pending.catch(error => this.report(`exec: ${String(error)}`));
236
+ return pending;
237
+ },
176
238
  events: {
177
239
  on: (name, handler) => { const listeners = this.bus.get(name) ?? new Set(); this.bus.set(name, listeners); listeners.add(handler); return () => { listeners.delete(handler); }; },
178
240
  emit: (name, data) => { for (const handler of this.bus.get(name) ?? []) try { handler(data); } catch (error) { this.report(String(error)); } },
@@ -183,7 +245,7 @@ export class ExtensionRegistry {
183
245
  async load(index: ResourceIndex): Promise<void> {
184
246
  const version = ++generation;
185
247
  for (const resource of index.entries.filter(r => r.kind === "extensions" && r.enabled)) {
186
- const maps = [this.commands, this.tools, this.shortcuts, this.flags, this.renderers, this.entryRenderers];
248
+ const maps = [this.commands, this.tools, this.shortcuts, this.flags, this.renderers, this.entryRenderers, this.integrations, this.integrationTools];
187
249
  const before = maps.map(map => new Set(map.keys()));
188
250
  const oldHandlers = new Map([...this.handlers].map(([name, handlers]) => [name, new Set(handlers)]));
189
251
  const oldBus = new Map([...this.bus].map(([name, handlers]) => [name, new Set(handlers)]));
@@ -218,36 +280,47 @@ export class ExtensionRegistry {
218
280
  return true;
219
281
  }
220
282
  definitions(): ReadonlyMap<string, ToolDefinition> {
221
- return new Map<string, ToolDefinition>([...this.tools].map(([name, tool]) => [name, {
283
+ const definitions = new Map<string, ToolDefinition>([...this.tools].map(([name, tool]) => [name, {
222
284
  name, description: tool.description, input_schema: extensionSchema(tool.parameters),
223
285
  // Arbitrary local code is not provably read-only. Keep admission and effects
224
286
  // serial; an extension's annotation cannot grant a parallel write capability.
225
287
  access: "write", execution: "exclusive", branch: "none",
288
+ ...(this.integrationTools.has(name) ? { integrationHealth: () => this.integrationTools.get(name)!.observe() } : {}),
226
289
  ...(tool.prepareArguments ? { prepareArguments: tool.prepareArguments } : {}),
227
290
  run: async (ctx: ToolContext, raw: Record<string, unknown>) => {
228
- let nested = 0;
229
291
  const context = { ...this.active().context(), signal: ctx.signal, tools: this.active().getTools(),
230
292
  executeTool: async (target: string, input: unknown) => {
231
- if (++nested > 32 || (ctx.extensionDepth ?? 0) >= 32 || target === name) return { isError: true, content: [{ type: "text", text: "Nested tool recursion refused" }] };
232
- const out = await runTool({ ...ctx, extensionDepth: (ctx.extensionDepth ?? 0) + 1, callId: `${ctx.callId ?? name}/${nested}` }, target, input);
233
- return { isError: !out.ok, content: [{ type: "text", text: out.content }], details: out.data };
293
+ if (!ctx.invoke) return { isError: true, content: [{ type: "text", text: "Nested tools require a live Session execution scope" }] };
294
+ const out = await ctx.invoke(target, input);
295
+ return { isError: !out.ok, content: [{ type: "text", text: out.content }], details: out.data,
296
+ outcome: out.outcome, diagnostic: out.diagnostic, warnings: out.warnings, artifact: out.content_ref, observation: out.observation };
234
297
  } };
235
- const result = await tool.execute(ctx.callId ?? name, raw, ctx.signal, update => {
298
+ const update = (update: ExtensionToolResult) => {
236
299
  for (const listener of this.toolUpdateListeners) try { listener(name, ctx.callId ?? name, raw, update); } catch (error) { this.report(String(error)); }
237
300
  ctx.onOutput?.(new TextEncoder().encode(update.content.filter(c => c.type === "text").map(c => c.text).join("\n")));
238
- }, context);
301
+ };
302
+ const integration = this.integrationTools.get(name);
303
+ const managed = integration ? await this.execution.run(ctx, () => integration.execute(name, ctx.callId ?? name, raw, ctx.signal, update, context)) : undefined;
304
+ if (managed && !managed.ok) return managed.result;
305
+ const result = managed?.ok ? managed.result : await this.execution.run(ctx, () => tool.execute(ctx.callId ?? name, raw, ctx.signal, update, context));
239
306
  const text = result.content.filter(c => c.type === "text").map(c => c.text).join("\n");
240
307
  const images = [];
241
308
  for (const item of result.content) if (item.type === "image" && ctx.imageStore) images.push(await attachImageBytes(Buffer.from(item.data, "base64"), name + ".png", ctx.imageStore, ctx.imageAutoResize));
242
309
  return { ok: result.isError !== true && !ctx.signal?.aborted, content: text,
243
- ...(isJsonValue(result.details) ? { data: result.details } : {}), ...(images.length ? { images } : {}) };
310
+ ...(managed?.ok ? { data: { kind: "integration_observation", version: 1, phase: "call", health: managed.observation,
311
+ ...(isJsonValue(result.details) ? { result: result.details } : {}) } } : isJsonValue(result.details) ? { data: result.details } : {}),
312
+ ...(images.length ? { images } : {}) };
244
313
  },
245
314
  }]));
315
+ if ([...this.tools.values()].some(tool => tool.exposure === "deferred")) definitions.set(DISCOVERY_TOOL_NAME, DISCOVERY_TOOL);
316
+ return definitions;
246
317
  }
247
318
  toolHooks(): NonNullable<ToolContext["extensionHooks"]> {
248
319
  return {
249
- before: async (toolName, input, toolCallId) => {
250
- const blocked = (await this.emit("tool_call", { toolName, input, toolCallId })).find(result => result["block"] === true);
320
+ scope: (ctx, action) => this.execution.run(ctx, action),
321
+ serial: () => ["tool_call", "tool_execution_start", "tool_execution_update", "tool_result", "tool_execution_end"].some(type => (this.handlers.get(type)?.size ?? 0) > 0),
322
+ before: async (toolName, input, toolCallId, parentToolCallId) => {
323
+ const blocked = (await this.emit("tool_call", { toolName, input, toolCallId, ...(parentToolCallId ? { parentToolCallId } : {}) })).find(result => result["block"] === true);
251
324
  return blocked ? String(blocked["reason"] ?? "Blocked by extension") : undefined;
252
325
  },
253
326
  event: async (type, fields) => { await this.emit(type, fields); },
@@ -255,12 +328,18 @@ export class ExtensionRegistry {
255
328
  }
256
329
  async dispose(): Promise<void> {
257
330
  if (this.disposed) return;
331
+ await Promise.all([...this.integrations.values()].map(owner => owner.dispose()));
258
332
  for (const detach of this.detachListeners) try { detach(); } catch (error) { this.report(String(error)); }
259
333
  this.detachListeners.clear(); this.toolUpdateListeners.clear();
260
334
  await this.emit("session_shutdown"); await this.settle(); this.disposed = true;
261
335
  for (const dispose of this.disposers.reverse()) try { dispose(); } catch (error) { this.report(String(error)); }
262
336
  this.handlers.clear(); this.bus.clear(); this.actions = undefined;
263
337
  }
338
+ /** Q.13: a new generation must not overlap an unresolved callback from its predecessor. */
339
+ async releaseIntegrations(): Promise<boolean> {
340
+ const released = await Promise.all([...this.integrations.values()].map(owner => owner.releaseForReload()));
341
+ return released.every(Boolean);
342
+ }
264
343
  }
265
344
 
266
345
  function extensionSchema(parameters: unknown): ToolDefinition["input_schema"] {
@@ -8,6 +8,8 @@ import { Session } from "./loop.ts";
8
8
  import { buildRequest, OpenAICompletionsProvider } from "./openai.ts";
9
9
  import type { Provider, ProviderConfig, ToolSchema, Turn } from "./provider.ts";
10
10
  import { DEFAULT_TOOL_CONTEXT, TOOL_SCHEMAS, runTool, type ToolResult } from "./tools.ts";
11
+ import { openTrialControl } from "./trial-control.ts";
12
+ import type { TrialLifecycle } from "../core/trial-lifecycle.ts";
11
13
 
12
14
  export const COMPARISON_TOOLS = ["read", "grep", "find", "ls", "write", "edit", "query"] as const;
13
15
  export const COMPARISON_SYSTEM = "Use the supplied file tools to solve the user's task. " +
@@ -202,6 +204,7 @@ export function comparisonGateway(options: {
202
204
  }
203
205
 
204
206
  export interface ComparisonResult {
207
+ lifecycle?: TrialLifecycle;
205
208
  arm: ComparisonArm;
206
209
  status: "answered" | "error" | "budget" | "balance" | "stopped";
207
210
  response: string;
@@ -221,20 +224,25 @@ export async function runComparison(options: {
221
224
  arm: ComparisonArm; directory: string; workspace: string; prompt: string; config: ProviderConfig;
222
225
  price: BenchmarkPrice; ledger: ComparisonLedger; backend: ComparisonBackend;
223
226
  maxRequests: number; timeoutMs: number; pi: PiIdentity | null; extension: string;
227
+ signal?: AbortSignal;
224
228
  onRequest?: (record: GatewayRecord) => Promise<void>;
225
229
  /** Synthetic pressure only; live comparisons preserve Pi's default compaction. */
226
230
  piCompaction?: { reserveTokens: number; keepRecentTokens: number };
227
231
  }): Promise<ComparisonResult> {
228
232
  const started = performance.now(), outputStore = join(options.directory, "observations");
229
233
  await mkdir(outputStore, { recursive: true });
230
- const gateway = comparisonGateway({ ...options, record: async (record) => {
234
+ const gateway = comparisonGateway({ ...options, backend: async (payload, signal) => {
235
+ control.beginBackend();
236
+ return options.backend(payload, AbortSignal.any([signal, control.signal]));
237
+ }, record: async (record) => {
231
238
  await appendFile(join(options.directory, "requests.jsonl"), JSON.stringify(record) + "\n");
232
239
  await options.onRequest?.(record);
233
240
  }, executeTool: (name, input) => runTool({ ...DEFAULT_TOOL_CONTEXT, root: options.workspace, allow: COMPARISON_TOOLS, outputStore }, name, input) });
234
241
  let response = "", cuts = 0, toolCalls = 0, toolFailures = 0, reason = "no completion";
235
242
  let status: ComparisonResult["status"] = "stopped";
236
243
  const config = { ...options.config, spec: { ...options.config.spec, baseUrl: gateway.url } };
237
- const signal = AbortSignal.timeout(options.timeoutMs);
244
+ const control = await openTrialControl(options.timeoutMs, options.signal, () => gateway.close()), signal = control.signal;
245
+ let session: Session | undefined, cleanupFailed = false;
238
246
  try {
239
247
  if (options.arm !== "pi") {
240
248
  await mkdir(join(options.directory, "log"));
@@ -244,10 +252,11 @@ export async function runComparison(options: {
244
252
  response = turn.blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
245
253
  return turn;
246
254
  } };
247
- const session = new Session({ logDir: join(options.directory, "log"), frozen: { system: COMPARISON_SYSTEM, tools: comparisonTools() },
255
+ session = new Session({ logDir: join(options.directory, "log"), frozen: { system: COMPARISON_SYSTEM, tools: comparisonTools() },
248
256
  tools: { ...DEFAULT_TOOL_CONTEXT, root: options.workspace, allow: COMPARISON_TOOLS, outputStore },
249
257
  contextPolicy: options.arm === "xeno-windowed" ? "windowed" : "continuous", maxTurns: options.maxRequests, harnessReviews: false }, provider);
250
258
  await session.start();
259
+ control.enterExecution();
251
260
  for await (const event of session.send(options.prompt, signal)) {
252
261
  if (event.kind === "cut") cuts++;
253
262
  if (event.kind === "tool_call") toolCalls++;
@@ -257,21 +266,32 @@ export async function runComparison(options: {
257
266
  if (["stop", "end_turn"].includes(reason)) status = "answered";
258
267
  } else {
259
268
  if (!options.pi) throw new Error("Pi identity required");
269
+ control.enterExecution();
260
270
  const run = await runPiPrompt({ directory: options.directory, workspace: options.workspace, prompt: options.prompt, config,
261
271
  gateway, price: options.price, pi: options.pi, extension: options.extension, signal, resume: false,
262
272
  ...(options.piCompaction ? { piCompaction: options.piCompaction } : {}) });
263
273
  ({ response, reason, cuts, toolCalls, toolFailures } = run);
264
274
  status = run.answered ? "answered" : "error";
265
275
  }
266
- } catch { status = "error"; reason = signal.aborted ? "timeout" : "harness failed; inspect local request and session evidence"; }
267
- finally { await gateway.close(); }
276
+ } catch { status = "error"; reason = "harness failed; inspect local request and session evidence"; }
277
+ finally {
278
+ control.finishWork();
279
+ try { await session?.dispose(); } catch { cleanupFailed = true; options.ledger.halted = true; }
280
+ try { await gateway.close(); } catch { cleanupFailed = true; options.ledger.halted = true; }
281
+ }
268
282
  if (gateway.stopped) {
269
283
  status = gateway.stopped === "budget" || gateway.stopped === "balance" ? gateway.stopped : gateway.stopped === "limit" ? "stopped" : "error";
270
- reason = signal.aborted ? "timeout" : `model gateway: ${gateway.stopped}`;
284
+ reason = `model gateway: ${gateway.stopped}`;
285
+ }
286
+ const lifecycle = control.observe(cleanupFailed ? "failed" : "complete");
287
+ if (lifecycle.interruption) {
288
+ status = lifecycle.interruption.source === "deadline" ? "error" : "stopped";
289
+ reason = lifecycle.interruption.source === "deadline" ? "timeout" : "cancelled";
271
290
  }
291
+ if (cleanupFailed) { status = "error"; reason = "execution_error"; }
272
292
  const usage: BenchmarkUsage = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
273
293
  for (const row of gateway.records) if (row.usage) for (const key of Object.keys(usage) as (keyof BenchmarkUsage)[]) usage[key] += row.usage[key];
274
- return { arm: options.arm, status, response, requests: gateway.requests, cuts, toolCalls, toolFailures,
294
+ return { arm: options.arm, status, response, requests: gateway.requests, cuts, toolCalls, toolFailures, lifecycle,
275
295
  charged: gateway.records.reduce((n, row) => n + row.charge, 0),
276
296
  reservedUnknown: gateway.records.reduce((n, row) => n + (row.measured === null ? row.charge : 0), 0),
277
297
  usage, wallMs: performance.now() - started, reason,