@bastani/atomic 0.8.28 → 0.8.29-alpha.2

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 (134) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +27 -0
  3. package/dist/builtin/cursor/LICENSE +26 -0
  4. package/dist/builtin/cursor/README.md +22 -0
  5. package/dist/builtin/cursor/index.ts +9 -0
  6. package/dist/builtin/cursor/package.json +46 -0
  7. package/dist/builtin/cursor/src/auth.ts +352 -0
  8. package/dist/builtin/cursor/src/catalog-cache.ts +155 -0
  9. package/dist/builtin/cursor/src/config.ts +123 -0
  10. package/dist/builtin/cursor/src/conversation-state.ts +135 -0
  11. package/dist/builtin/cursor/src/cursor-models-raw.json +583 -0
  12. package/dist/builtin/cursor/src/model-mapper.ts +270 -0
  13. package/dist/builtin/cursor/src/models.ts +54 -0
  14. package/dist/builtin/cursor/src/native-loader.ts +71 -0
  15. package/dist/builtin/cursor/src/proto/README.md +34 -0
  16. package/dist/builtin/cursor/src/proto/agent_pb.ts +15294 -0
  17. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +717 -0
  18. package/dist/builtin/cursor/src/provider.ts +301 -0
  19. package/dist/builtin/cursor/src/stream.ts +564 -0
  20. package/dist/builtin/cursor/src/transport.ts +791 -0
  21. package/dist/builtin/intercom/CHANGELOG.md +4 -0
  22. package/dist/builtin/intercom/package.json +2 -2
  23. package/dist/builtin/intercom/skills/intercom/SKILL.md +5 -5
  24. package/dist/builtin/mcp/CHANGELOG.md +4 -0
  25. package/dist/builtin/mcp/package.json +3 -3
  26. package/dist/builtin/subagents/CHANGELOG.md +12 -0
  27. package/dist/builtin/subagents/README.md +7 -3
  28. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -24
  29. package/dist/builtin/subagents/agents/debugger.md +3 -5
  30. package/dist/builtin/subagents/package.json +4 -4
  31. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +2 -1
  32. package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -1
  33. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
  34. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +19 -2
  35. package/dist/builtin/subagents/src/runs/shared/structured-output.ts +271 -10
  36. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +12 -39
  37. package/dist/builtin/subagents/src/shared/types.ts +1 -0
  38. package/dist/builtin/subagents/src/shared/utils.ts +50 -10
  39. package/dist/builtin/subagents/src/slash/saved-chain-mapping.ts +77 -0
  40. package/dist/builtin/subagents/src/slash/slash-commands.ts +1 -55
  41. package/dist/builtin/web-access/CHANGELOG.md +5 -1
  42. package/dist/builtin/web-access/README.md +1 -1
  43. package/dist/builtin/web-access/github-extract.ts +1 -1
  44. package/dist/builtin/web-access/package.json +3 -3
  45. package/dist/builtin/workflows/CHANGELOG.md +18 -0
  46. package/dist/builtin/workflows/README.md +19 -1
  47. package/dist/builtin/workflows/package.json +2 -2
  48. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +17 -3
  49. package/dist/builtin/workflows/src/extension/wiring.ts +17 -1
  50. package/dist/builtin/workflows/src/extension/workflow-schema.ts +34 -0
  51. package/dist/builtin/workflows/src/runs/foreground/executor.ts +13 -2
  52. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +86 -14
  53. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +11 -3
  54. package/dist/builtin/workflows/src/shared/types.ts +8 -4
  55. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +64 -2
  56. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  57. package/dist/builtin/workflows/src/tui/workflow-status.ts +2 -0
  58. package/dist/core/builtin-packages.d.ts.map +1 -1
  59. package/dist/core/builtin-packages.js +6 -0
  60. package/dist/core/builtin-packages.js.map +1 -1
  61. package/dist/core/extensions/index.d.ts +1 -1
  62. package/dist/core/extensions/index.d.ts.map +1 -1
  63. package/dist/core/extensions/index.js.map +1 -1
  64. package/dist/core/extensions/types.d.ts +20 -0
  65. package/dist/core/extensions/types.d.ts.map +1 -1
  66. package/dist/core/extensions/types.js.map +1 -1
  67. package/dist/core/model-resolver.d.ts +1 -0
  68. package/dist/core/model-resolver.d.ts.map +1 -1
  69. package/dist/core/model-resolver.js +17 -8
  70. package/dist/core/model-resolver.js.map +1 -1
  71. package/dist/core/package-manager.d.ts +11 -9
  72. package/dist/core/package-manager.d.ts.map +1 -1
  73. package/dist/core/package-manager.js +55 -10
  74. package/dist/core/package-manager.js.map +1 -1
  75. package/dist/core/project-trust.d.ts +1 -0
  76. package/dist/core/project-trust.d.ts.map +1 -1
  77. package/dist/core/project-trust.js +3 -3
  78. package/dist/core/project-trust.js.map +1 -1
  79. package/dist/core/resource-loader.d.ts +9 -0
  80. package/dist/core/resource-loader.d.ts.map +1 -1
  81. package/dist/core/resource-loader.js +72 -9
  82. package/dist/core/resource-loader.js.map +1 -1
  83. package/dist/core/sdk.d.ts +3 -3
  84. package/dist/core/sdk.d.ts.map +1 -1
  85. package/dist/core/sdk.js +5 -5
  86. package/dist/core/sdk.js.map +1 -1
  87. package/dist/core/tools/index.d.ts +1 -0
  88. package/dist/core/tools/index.d.ts.map +1 -1
  89. package/dist/core/tools/index.js +1 -0
  90. package/dist/core/tools/index.js.map +1 -1
  91. package/dist/core/tools/structured-output.d.ts +39 -0
  92. package/dist/core/tools/structured-output.d.ts.map +1 -0
  93. package/dist/core/tools/structured-output.js +141 -0
  94. package/dist/core/tools/structured-output.js.map +1 -0
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +1 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/main.d.ts.map +1 -1
  100. package/dist/main.js +36 -14
  101. package/dist/main.js.map +1 -1
  102. package/dist/modes/interactive/components/login-dialog.d.ts +3 -0
  103. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/login-dialog.js +16 -0
  105. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.d.ts +11 -0
  107. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  108. package/dist/modes/interactive/interactive-mode.js +158 -11
  109. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  110. package/dist/modes/print-mode.d.ts.map +1 -1
  111. package/dist/modes/print-mode.js +39 -0
  112. package/dist/modes/print-mode.js.map +1 -1
  113. package/docs/custom-provider.md +1 -0
  114. package/docs/extensions.md +2 -2
  115. package/docs/models.md +2 -0
  116. package/docs/packages.md +3 -1
  117. package/docs/providers.md +15 -0
  118. package/docs/sdk.md +61 -0
  119. package/docs/security.md +1 -1
  120. package/docs/subagents.md +21 -0
  121. package/docs/usage.md +2 -0
  122. package/docs/workflows.md +10 -7
  123. package/examples/extensions/README.md +1 -1
  124. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  125. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  126. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  127. package/examples/extensions/gondolin/package-lock.json +2 -2
  128. package/examples/extensions/gondolin/package.json +1 -1
  129. package/examples/extensions/sandbox/package-lock.json +2 -2
  130. package/examples/extensions/sandbox/package.json +1 -1
  131. package/examples/extensions/structured-output.ts +22 -53
  132. package/examples/extensions/with-deps/package-lock.json +2 -2
  133. package/examples/extensions/with-deps/package.json +1 -1
  134. package/package.json +12 -9
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { APP_NAME } from "@bastani/atomic";
4
+ import { APP_NAME, STRUCTURED_OUTPUT_TOOL_NAME, getStructuredOutputMetadataPath } from "@bastani/atomic";
5
5
  import { Compile } from "typebox/compile";
6
6
  import type { JsonSchemaObject } from "../../shared/types.ts";
7
7
 
@@ -9,10 +9,43 @@ const ENV_PREFIX = APP_NAME.toUpperCase();
9
9
  export const STRUCTURED_OUTPUT_SCHEMA_ENV = `${ENV_PREFIX}_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA`;
10
10
  export const STRUCTURED_OUTPUT_CAPTURE_ENV = `${ENV_PREFIX}_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE`;
11
11
 
12
+ type JsonPrimitive = string | number | boolean | null;
13
+ type JsonArray = readonly JsonValue[];
14
+ type JsonRecord = { readonly [key: string]: JsonValue };
15
+ type JsonValue = JsonPrimitive | JsonArray | JsonRecord;
16
+
12
17
  export interface StructuredOutputRuntime {
13
18
  schema: JsonSchemaObject;
14
19
  schemaPath: string;
15
20
  outputPath: string;
21
+ metadataPath: string;
22
+ }
23
+
24
+ export interface StructuredOutputCaptureMetadata {
25
+ toolName: string;
26
+ toolCallId: string;
27
+ success: true;
28
+ terminate: true;
29
+ capturedAt?: string;
30
+ }
31
+
32
+ export interface StructuredOutputTranscriptContent {
33
+ readonly type?: string;
34
+ readonly id?: string;
35
+ readonly name?: string;
36
+ }
37
+
38
+ export interface StructuredOutputTranscriptMessage {
39
+ readonly role: string;
40
+ readonly content?: string | readonly StructuredOutputTranscriptContent[];
41
+ readonly toolCallId?: string;
42
+ readonly toolName?: string;
43
+ readonly isError?: boolean;
44
+ }
45
+
46
+ export interface ReadStructuredOutputOptions {
47
+ messages?: readonly StructuredOutputTranscriptMessage[];
48
+ toolName?: string;
16
49
  }
17
50
 
18
51
  interface CompiledJsonSchema {
@@ -20,21 +53,223 @@ interface CompiledJsonSchema {
20
53
  Errors(value: unknown): Iterable<{ instancePath?: string; message?: string }>;
21
54
  }
22
55
 
23
- export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"): asserts schema is JsonSchemaObject {
56
+ type JsonSchemaRootDescriptor = {
57
+ readonly type?: string | readonly string[];
58
+ readonly anyOf?: readonly JsonSchemaObject[];
59
+ readonly oneOf?: readonly JsonSchemaObject[];
60
+ readonly allOf?: readonly JsonSchemaObject[];
61
+ };
62
+
63
+ type ToolCallBlock = {
64
+ readonly type: "toolCall";
65
+ readonly id?: string;
66
+ readonly name?: string;
67
+ };
68
+
69
+ function schemaTypeIsObjectOnly(type: JsonSchemaRootDescriptor["type"]): boolean {
70
+ if (type === "object") return true;
71
+ return Array.isArray(type) && type.length === 1 && type[0] === "object";
72
+ }
73
+
74
+ function isTopLevelObjectOutputSchema(schema: JsonSchemaObject): boolean {
75
+ const descriptor = schema as JsonSchemaRootDescriptor;
76
+ if (schemaTypeIsObjectOnly(descriptor.type)) return true;
77
+ if (descriptor.type !== undefined) return false;
78
+ if (Array.isArray(descriptor.anyOf) || Array.isArray(descriptor.oneOf)) return false;
79
+ if (Array.isArray(descriptor.allOf)) {
80
+ return descriptor.allOf.length > 0 && descriptor.allOf.every((member) => isTopLevelObjectOutputSchema(member));
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function isJsonRecord(value: JsonValue): value is JsonRecord {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value);
87
+ }
88
+
89
+ function stringField(record: JsonRecord, key: string): string | undefined {
90
+ const value = record[key];
91
+ return typeof value === "string" && value.length > 0 ? value : undefined;
92
+ }
93
+
94
+ function booleanField(record: JsonRecord, key: string): boolean | undefined {
95
+ const value = record[key];
96
+ return typeof value === "boolean" ? value : undefined;
97
+ }
98
+
99
+ function roleOf(message: StructuredOutputTranscriptMessage): string {
100
+ return message.role;
101
+ }
102
+
103
+ function toolCallBlocks(message: StructuredOutputTranscriptMessage): ToolCallBlock[] {
104
+ if (roleOf(message) !== "assistant" || !Array.isArray(message.content)) return [];
105
+ return message.content
106
+ .filter((block): block is ToolCallBlock => block.type === "toolCall")
107
+ .map((block) => ({ type: "toolCall", id: block.id, name: block.name }));
108
+ }
109
+
110
+ function isFinalityRelevantMessage(message: StructuredOutputTranscriptMessage): boolean {
111
+ const role = roleOf(message);
112
+ // `custom` entries are host/runtime annotations (for example display/status
113
+ // messages) rather than additional child model output, so they should not make
114
+ // an otherwise-final structured_output capture look stale.
115
+ return role === "assistant" || role === "toolResult";
116
+ }
117
+
118
+ function finalityInvalid(message: string): { status: "invalid"; message: string } {
119
+ return { status: "invalid", message };
120
+ }
121
+
122
+ function parseCaptureMetadata(value: JsonValue): { metadata?: StructuredOutputCaptureMetadata; error?: string } {
123
+ if (!isJsonRecord(value)) {
124
+ return { error: "Structured output metadata sidecar must contain an object with call metadata." };
125
+ }
126
+ const toolName = stringField(value, "toolName");
127
+ const toolCallId = stringField(value, "toolCallId");
128
+ if (!toolName || !toolCallId) {
129
+ return { error: "Structured output metadata sidecar is missing toolName or toolCallId metadata." };
130
+ }
131
+ if (booleanField(value, "success") !== true) {
132
+ return { error: "Structured output capture was not marked successful." };
133
+ }
134
+ if (booleanField(value, "terminate") !== true) {
135
+ return { error: "Structured output capture was not marked as a terminating action." };
136
+ }
137
+ return {
138
+ metadata: {
139
+ toolName,
140
+ toolCallId,
141
+ success: true,
142
+ terminate: true,
143
+ ...(typeof value.capturedAt === "string" ? { capturedAt: value.capturedAt } : {}),
144
+ },
145
+ };
146
+ }
147
+
148
+ function verifyStructuredOutputFinality(
149
+ messages: readonly StructuredOutputTranscriptMessage[],
150
+ metadata: StructuredOutputCaptureMetadata,
151
+ expectedToolName: string,
152
+ ): { status: "valid" } | { status: "invalid"; message: string } {
153
+ if (metadata.toolName !== expectedToolName) {
154
+ return finalityInvalid(
155
+ `Captured structured output tool name ${JSON.stringify(metadata.toolName)} did not match expected ${JSON.stringify(expectedToolName)}.`,
156
+ );
157
+ }
158
+ if (messages.length === 0) {
159
+ return finalityInvalid("Structured output finality could not be verified because the child transcript is empty.");
160
+ }
161
+
162
+ let structuredOutputCallCount = 0;
163
+ let assistantIndex = -1;
164
+ let matchingAssistantToolCalls: ToolCallBlock[] = [];
165
+ for (let index = 0; index < messages.length; index++) {
166
+ const calls = toolCallBlocks(messages[index]);
167
+ if (calls.length === 0) continue;
168
+ for (const call of calls) {
169
+ if (call.name === metadata.toolName) {
170
+ structuredOutputCallCount += 1;
171
+ }
172
+ }
173
+ const idMatch = calls.find((call) => call.id === metadata.toolCallId);
174
+ if (!idMatch) continue;
175
+ if (idMatch.name !== metadata.toolName) {
176
+ return finalityInvalid(
177
+ `Captured structured output tool call ${JSON.stringify(metadata.toolCallId)} used tool name ${JSON.stringify(idMatch.name)} instead of ${JSON.stringify(metadata.toolName)}.`,
178
+ );
179
+ }
180
+ assistantIndex = index;
181
+ matchingAssistantToolCalls = calls;
182
+ }
183
+
184
+ if (structuredOutputCallCount > 1) {
185
+ return finalityInvalid(
186
+ `Captured structured output call ${JSON.stringify(metadata.toolCallId)} was not exactly once; another ${metadata.toolName} tool call appeared in the child transcript.`,
187
+ );
188
+ }
189
+ if (assistantIndex === -1) {
190
+ return finalityInvalid(
191
+ `No assistant tool call matched captured structured output toolCallId ${JSON.stringify(metadata.toolCallId)}.`,
192
+ );
193
+ }
194
+ if (matchingAssistantToolCalls.length !== 1) {
195
+ return finalityInvalid(
196
+ `Captured structured output call ${JSON.stringify(metadata.toolCallId)} was accompanied by sibling tool calls in the same assistant message.`,
197
+ );
198
+ }
199
+
200
+ let resultIndex = -1;
201
+ let resultMessage: StructuredOutputTranscriptMessage | undefined;
202
+ for (let index = assistantIndex + 1; index < messages.length; index++) {
203
+ const message = messages[index];
204
+ if (roleOf(message) !== "toolResult") continue;
205
+ if (message.toolCallId !== metadata.toolCallId) continue;
206
+ resultIndex = index;
207
+ resultMessage = message;
208
+ break;
209
+ }
210
+
211
+ if (!resultMessage) {
212
+ return finalityInvalid(
213
+ `No tool result matched captured structured output toolCallId ${JSON.stringify(metadata.toolCallId)}.`,
214
+ );
215
+ }
216
+ if (resultMessage.toolName !== metadata.toolName) {
217
+ return finalityInvalid(
218
+ `Structured output tool result for ${JSON.stringify(metadata.toolCallId)} used tool name ${JSON.stringify(resultMessage.toolName)} instead of ${JSON.stringify(metadata.toolName)}.`,
219
+ );
220
+ }
221
+ if (resultMessage.isError !== false) {
222
+ return finalityInvalid(
223
+ `Structured output tool result for ${JSON.stringify(metadata.toolCallId)} was an error or did not prove success.`,
224
+ );
225
+ }
226
+
227
+ for (let index = assistantIndex + 1; index < resultIndex; index++) {
228
+ const message = messages[index];
229
+ if (isFinalityRelevantMessage(message)) {
230
+ return finalityInvalid(
231
+ `Structured output call ${JSON.stringify(metadata.toolCallId)} was not final; another ${roleOf(message)} message appeared before its matching tool result.`,
232
+ );
233
+ }
234
+ }
235
+ for (let index = resultIndex + 1; index < messages.length; index++) {
236
+ const message = messages[index];
237
+ if (isFinalityRelevantMessage(message)) {
238
+ return finalityInvalid(
239
+ `Structured output call ${JSON.stringify(metadata.toolCallId)} was not final; a later ${roleOf(message)} message followed the successful tool result.`,
240
+ );
241
+ }
242
+ }
243
+
244
+ return { status: "valid" };
245
+ }
246
+
247
+ export function assertJsonSchemaDescriptor(schema: unknown, label = "outputSchema"): asserts schema is JsonSchemaObject {
24
248
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
25
- throw new Error(`${label} must be a JSON Schema object.`);
249
+ throw new Error(`${label} must be a JSON Schema object descriptor.`);
250
+ }
251
+ }
252
+
253
+ export function assertStructuredOutputParameterSchema(schema: unknown, label = "outputSchema"): asserts schema is JsonSchemaObject {
254
+ assertJsonSchemaDescriptor(schema, label);
255
+ if (!isTopLevelObjectOutputSchema(schema)) {
256
+ throw new Error(
257
+ `${label} must be a top-level object tool-argument schema. `
258
+ + "Wrap array or primitive outputs in an object field, for example `{ items: [...] }` or `{ value: ... }`.",
259
+ );
26
260
  }
27
261
  }
28
262
 
29
263
  export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string): StructuredOutputRuntime {
30
- assertJsonSchemaObject(schema);
264
+ assertStructuredOutputParameterSchema(schema);
31
265
  const rootDir = baseDir ?? os.tmpdir();
32
266
  fs.mkdirSync(rootDir, { recursive: true });
33
267
  const dir = fs.mkdtempSync(path.join(rootDir, "pi-subagent-structured-"));
34
268
  const schemaPath = path.join(dir, "schema.json");
35
269
  const outputPath = path.join(dir, "output.json");
270
+ const metadataPath = path.join(dir, "output.meta.json");
36
271
  fs.writeFileSync(schemaPath, JSON.stringify(schema), { mode: 0o600 });
37
- return { schema, schemaPath, outputPath };
272
+ return { schema, schemaPath, outputPath, metadataPath };
38
273
  }
39
274
 
40
275
  export function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): { status: "valid" } | { status: "invalid"; message: string } {
@@ -54,19 +289,45 @@ export function validateStructuredOutputValue(schema: JsonSchemaObject, value: u
54
289
  return { status: "invalid", message: errors.join("; ") || "schema validation failed" };
55
290
  }
56
291
 
57
- export function readStructuredOutput(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
292
+ export function readStructuredOutput(
293
+ runtime: StructuredOutputRuntime,
294
+ options: ReadStructuredOutputOptions = {},
295
+ ): { value?: unknown; error?: string } {
58
296
  if (!fs.existsSync(runtime.outputPath)) {
59
297
  return { error: "Missing structured_output call; this step has outputSchema and must finish by calling structured_output." };
60
298
  }
61
- let value: unknown;
299
+ const metadataPath = runtime.metadataPath ?? getStructuredOutputMetadataPath(runtime.outputPath);
300
+ if (!fs.existsSync(metadataPath)) {
301
+ return { error: "Missing structured_output metadata sidecar; this step must finish with a verified structured_output call." };
302
+ }
303
+ let payload: JsonValue;
62
304
  try {
63
- value = JSON.parse(fs.readFileSync(runtime.outputPath, "utf-8"));
305
+ payload = JSON.parse(fs.readFileSync(runtime.outputPath, "utf-8")) as JsonValue;
64
306
  } catch (error) {
65
307
  return { error: `Failed to read structured output: ${error instanceof Error ? error.message : String(error)}` };
66
308
  }
67
- const validation = validateStructuredOutputValue(runtime.schema, value);
309
+ let rawMetadata: JsonValue;
310
+ try {
311
+ rawMetadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as JsonValue;
312
+ } catch (error) {
313
+ return { error: `Failed to read structured output metadata: ${error instanceof Error ? error.message : String(error)}` };
314
+ }
315
+ const parsed = parseCaptureMetadata(rawMetadata);
316
+ if (parsed.error || !parsed.metadata) {
317
+ return { error: parsed.error ?? "Structured output metadata sidecar is invalid." };
318
+ }
319
+ const validation = validateStructuredOutputValue(runtime.schema, payload);
68
320
  if (validation.status === "invalid") return { error: `Structured output validation failed: ${validation.message}` };
69
- return { value };
321
+ const expectedToolName = options.toolName ?? STRUCTURED_OUTPUT_TOOL_NAME;
322
+ if (options.messages) {
323
+ const finality = verifyStructuredOutputFinality(options.messages, parsed.metadata, expectedToolName);
324
+ if (finality.status === "invalid") return { error: finality.message };
325
+ } else if (parsed.metadata.toolName !== expectedToolName) {
326
+ return {
327
+ error: `Captured structured output tool name ${JSON.stringify(parsed.metadata.toolName)} did not match expected ${JSON.stringify(expectedToolName)}.`,
328
+ };
329
+ }
330
+ return { value: payload };
70
331
  }
71
332
 
72
333
  export function cleanupStructuredOutputRuntime(runtime: StructuredOutputRuntime | undefined): void {
@@ -1,14 +1,12 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { ExtensionAPI } from "@bastani/atomic";
4
- import { getEnvValue } from "@bastani/atomic";
2
+ import { createStructuredOutputTool, getEnvValue, type ExtensionAPI } from "@bastani/atomic";
5
3
  import {
6
4
  SUBAGENT_FANOUT_CHILD_ENV,
7
5
  SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV,
8
6
  SUBAGENT_INHERIT_SKILLS_ENV,
9
7
  SUBAGENT_INTERCOM_SESSION_NAME_ENV,
10
8
  } from "./pi-args.ts";
11
- import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
9
+ import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "./structured-output.ts";
12
10
  import type { JsonSchemaObject } from "../../shared/types.ts";
13
11
 
14
12
  export { SUBAGENT_INTERCOM_SESSION_NAME_ENV } from "./pi-args.ts";
@@ -16,6 +14,7 @@ export { SUBAGENT_INTERCOM_SESSION_NAME_ENV } from "./pi-args.ts";
16
14
  const STRUCTURED_OUTPUT_INSTRUCTIONS = [
17
15
  "This subagent step has a strict structured output contract.",
18
16
  "Your final action must be to call the `structured_output` tool with JSON matching the provided schema.",
17
+ "Pass the schema fields directly as the tool arguments; do not wrap them in `{ value: ... }` unless the schema explicitly defines a top-level `value` field.",
19
18
  "Do not rely on prose-only completion; if you do not call `structured_output`, the parent will fail this step.",
20
19
  ].join("\n");
21
20
 
@@ -162,44 +161,18 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
162
161
  const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
163
162
  if (structuredOutputPath && structuredSchemaPath) {
164
163
  const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
165
- const parameters = {
166
- type: "object",
167
- properties: { value: schema },
168
- required: ["value"],
169
- additionalProperties: false,
170
- };
171
- const registerTool = pi.registerTool as unknown as (tool: {
172
- name: string;
173
- label: string;
174
- description: string;
175
- parameters: unknown;
176
- execute: (_id: string, params: { value: unknown }) => Promise<unknown>;
177
- }) => void;
178
- registerTool({
179
- name: "structured_output",
180
- label: "Structured Output",
181
- description: "Submit the required final structured output for this subagent step. This terminates the step.",
182
- parameters: parameters as never,
183
- async execute(_id: string, params: { value: unknown }) {
184
- const validation = validateStructuredOutputValue(schema, params.value);
185
- if (validation.status === "invalid") {
186
- throw new Error(`Structured output validation failed: ${validation.message}`);
187
- }
188
- fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
189
- fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
190
- return {
191
- content: [{ type: "text", text: "Structured output captured." }],
192
- details: { path: structuredOutputPath },
193
- terminate: true,
194
- };
195
- },
196
- });
164
+ pi.registerTool(createStructuredOutputTool({
165
+ schema,
166
+ output: { outputPath: structuredOutputPath },
167
+ }));
197
168
  }
198
169
 
199
170
  const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown) => unknown) => void;
200
- onRuntimeEvent("context", (event: { messages: unknown[] }) => {
201
- const messages = stripParentOnlySubagentMessages(event.messages);
202
- if (messages === event.messages) return undefined;
171
+ onRuntimeEvent("context", (event) => {
172
+ const contextEvent = event as { messages?: unknown[] };
173
+ if (!Array.isArray(contextEvent.messages)) return undefined;
174
+ const messages = stripParentOnlySubagentMessages(contextEvent.messages);
175
+ if (messages === contextEvent.messages) return undefined;
203
176
  return { messages };
204
177
  });
205
178
 
@@ -803,6 +803,7 @@ export interface RunSyncOptions {
803
803
  schema: JsonSchemaObject;
804
804
  schemaPath: string;
805
805
  outputPath: string;
806
+ metadataPath: string;
806
807
  };
807
808
  acceptance?: AcceptanceInput;
808
809
  acceptanceContext?: {
@@ -122,21 +122,61 @@ export function findLatestSessionFile(sessionDir: string): string | null {
122
122
  // Message Parsing Utilities
123
123
  // ============================================================================
124
124
 
125
+ const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
126
+
127
+ type TextContentCandidate = {
128
+ type: string;
129
+ text?: string;
130
+ };
131
+
132
+ function getLastNonEmptyTextContent(content: readonly TextContentCandidate[]): string | undefined {
133
+ for (let index = content.length - 1; index >= 0; index--) {
134
+ const part = content[index];
135
+ if (part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0) {
136
+ return part.text;
137
+ }
138
+ }
139
+ return undefined;
140
+ }
141
+
142
+ function getCombinedNonEmptyTextContent(content: readonly TextContentCandidate[]): string | undefined {
143
+ let text = "";
144
+ for (const part of content) {
145
+ if (part.type === "text" && typeof part.text === "string") {
146
+ text += part.text;
147
+ }
148
+ }
149
+ return text.trim().length > 0 ? text : undefined;
150
+ }
151
+
152
+ function getStructuredOutputToolResultText(message: Message): string | undefined {
153
+ if (message.role !== "toolResult") return undefined;
154
+ if (message.toolName !== STRUCTURED_OUTPUT_TOOL_NAME) return undefined;
155
+ if (message.isError === true) return undefined;
156
+ return getCombinedNonEmptyTextContent(message.content);
157
+ }
158
+
159
+ function getAssistantOutputText(message: Message): string | undefined {
160
+ if (message.role !== "assistant") return undefined;
161
+ const hasAssistantError = ("errorMessage" in message && typeof message.errorMessage === "string" && message.errorMessage.length > 0)
162
+ || ("stopReason" in message && message.stopReason === "error");
163
+ if (hasAssistantError) return undefined;
164
+ return getLastNonEmptyTextContent(message.content);
165
+ }
166
+
125
167
  /**
126
168
  * Get the final text output from a list of messages
127
169
  */
128
170
  export function getFinalOutput(messages: Message[]): string {
171
+ const finalMessage = messages[messages.length - 1];
172
+ if (finalMessage) {
173
+ const finalStructuredOutput = getStructuredOutputToolResultText(finalMessage);
174
+ if (finalStructuredOutput !== undefined) return finalStructuredOutput;
175
+ }
176
+
129
177
  for (let i = messages.length - 1; i >= 0; i--) {
130
- const msg = messages[i];
131
- if (msg.role === "assistant") {
132
- const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
133
- || ("stopReason" in msg && msg.stopReason === "error");
134
- if (hasAssistantError) continue;
135
- for (let j = msg.content.length - 1; j >= 0; j--) {
136
- const part = msg.content[j];
137
- if (part.type === "text" && part.text.trim().length > 0) return part.text;
138
- }
139
- }
178
+ const assistantOutput = getAssistantOutputText(messages[i]);
179
+ if (assistantOutput !== undefined) return assistantOutput;
140
180
  }
141
181
  return "";
142
182
  }
@@ -0,0 +1,77 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ChainConfig } from "../agents/agents.ts";
4
+ import { assertJsonSchemaDescriptor, assertStructuredOutputParameterSchema } from "../runs/shared/structured-output.ts";
5
+ import { isDynamicParallelStep, isParallelStep, type ChainStep } from "../shared/settings.ts";
6
+ import type { JsonSchemaObject } from "../shared/types.ts";
7
+
8
+ function loadSavedOutputSchema(
9
+ chain: ChainConfig,
10
+ stepAgent: string,
11
+ outputSchema: unknown,
12
+ options: { schemaRole: "tool-parameters" | "collection" } = { schemaRole: "tool-parameters" },
13
+ ): JsonSchemaObject | undefined {
14
+ if (outputSchema === undefined) return undefined;
15
+ const labelForSchema = (schemaPath?: string): string => schemaPath
16
+ ? `outputSchema for chain '${chain.name}' step '${stepAgent}' (${schemaPath})`
17
+ : `outputSchema for chain '${chain.name}' step '${stepAgent}'`;
18
+ const validateSavedSchema = (schema: unknown, label: string): JsonSchemaObject => {
19
+ if (options.schemaRole === "collection") {
20
+ assertJsonSchemaDescriptor(schema, label);
21
+ } else {
22
+ assertStructuredOutputParameterSchema(schema, label);
23
+ }
24
+ return schema;
25
+ };
26
+ if (typeof outputSchema === "string") {
27
+ const schemaPath = path.isAbsolute(outputSchema)
28
+ ? outputSchema
29
+ : path.join(path.dirname(chain.filePath), outputSchema);
30
+ const parsed = JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as unknown;
31
+ return validateSavedSchema(parsed, labelForSchema(schemaPath));
32
+ }
33
+ return validateSavedSchema(outputSchema, labelForSchema());
34
+ }
35
+
36
+ export function mapSavedChainSteps(chain: ChainConfig, worktree = false): ChainStep[] {
37
+ return (chain.steps as unknown as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
38
+ if (isParallelStep(step)) {
39
+ const parallel = step.parallel.map((task) => {
40
+ const { outputSchema: rawOutputSchema, ...rest } = task as typeof task & { outputSchema?: unknown };
41
+ const outputSchema = loadSavedOutputSchema(chain, task.agent, rawOutputSchema);
42
+ return { ...rest, ...(outputSchema ? { outputSchema } : {}) };
43
+ });
44
+ return { ...step, parallel, ...(worktree ? { worktree: true } : {}) };
45
+ }
46
+ if (isDynamicParallelStep(step)) {
47
+ const { outputSchema: rawOutputSchema, ...parallelRest } = step.parallel as typeof step.parallel & { outputSchema?: unknown };
48
+ const outputSchema = loadSavedOutputSchema(chain, step.parallel.agent, rawOutputSchema);
49
+ const collectSchema = loadSavedOutputSchema(
50
+ chain,
51
+ `${step.collect.as} collection`,
52
+ step.collect.outputSchema,
53
+ { schemaRole: "collection" },
54
+ );
55
+ return {
56
+ ...step,
57
+ parallel: { ...parallelRest, ...(outputSchema ? { outputSchema } : {}) },
58
+ collect: { ...step.collect, ...(collectSchema ? { outputSchema: collectSchema } : {}) },
59
+ };
60
+ }
61
+ const outputSchema = loadSavedOutputSchema(chain, step.agent, (step as { outputSchema?: unknown }).outputSchema);
62
+ return {
63
+ agent: step.agent,
64
+ task: step.task || undefined,
65
+ ...(step.phase ? { phase: step.phase } : {}),
66
+ ...(step.label ? { label: step.label } : {}),
67
+ ...(step.as ? { as: step.as } : {}),
68
+ ...(outputSchema ? { outputSchema } : {}),
69
+ output: step.output,
70
+ outputMode: step.outputMode,
71
+ reads: step.reads,
72
+ progress: step.progress,
73
+ skill: step.skill ?? step.skills,
74
+ model: step.model,
75
+ };
76
+ });
77
+ }
@@ -5,8 +5,7 @@ import type { ExtensionAPI, ExtensionContext } from "@bastani/atomic";
5
5
  import { Key, matchesKey } from "@earendil-works/pi-tui";
6
6
  import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
7
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
8
- import { isDynamicParallelStep, isParallelStep, type ChainStep } from "../shared/settings.ts";
9
- import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
8
+ import { mapSavedChainSteps } from "./saved-chain-mapping.ts";
10
9
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
11
10
  import {
12
11
  applySlashUpdate,
@@ -21,7 +20,6 @@ import {
21
20
  SLASH_SUBAGENT_RESPONSE_EVENT,
22
21
  SLASH_SUBAGENT_STARTED_EVENT,
23
22
  SLASH_SUBAGENT_UPDATE_EVENT,
24
- type JsonSchemaObject,
25
23
  type SingleResult,
26
24
  type SubagentState,
27
25
  } from "../shared/types.ts";
@@ -125,58 +123,6 @@ const makeChainCompletions = (state: SubagentState) => (prefix: string) => {
125
123
  .map((chain) => ({ value: chain.name, label: chain.name }));
126
124
  };
127
125
 
128
- function loadSavedOutputSchema(chain: ChainConfig, stepAgent: string, outputSchema: unknown): JsonSchemaObject | undefined {
129
- if (outputSchema === undefined) return undefined;
130
- if (typeof outputSchema === "string") {
131
- const schemaPath = path.isAbsolute(outputSchema)
132
- ? outputSchema
133
- : path.join(path.dirname(chain.filePath), outputSchema);
134
- const parsed = JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as unknown;
135
- assertJsonSchemaObject(parsed, `outputSchema for chain '${chain.name}' step '${stepAgent}' (${schemaPath})`);
136
- return parsed;
137
- }
138
- assertJsonSchemaObject(outputSchema, `outputSchema for chain '${chain.name}' step '${stepAgent}'`);
139
- return outputSchema;
140
- }
141
-
142
- const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] => {
143
- return (chain.steps as unknown as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
144
- if (isParallelStep(step)) {
145
- const parallel = step.parallel.map((task) => {
146
- const { outputSchema: rawOutputSchema, ...rest } = task as typeof task & { outputSchema?: unknown };
147
- const outputSchema = loadSavedOutputSchema(chain, task.agent, rawOutputSchema);
148
- return { ...rest, ...(outputSchema ? { outputSchema } : {}) };
149
- });
150
- return { ...step, parallel, ...(worktree ? { worktree: true } : {}) };
151
- }
152
- if (isDynamicParallelStep(step)) {
153
- const { outputSchema: rawOutputSchema, ...parallelRest } = step.parallel as typeof step.parallel & { outputSchema?: unknown };
154
- const outputSchema = loadSavedOutputSchema(chain, step.parallel.agent, rawOutputSchema);
155
- const collectSchema = loadSavedOutputSchema(chain, `${step.collect.as} collection`, step.collect.outputSchema);
156
- return {
157
- ...step,
158
- parallel: { ...parallelRest, ...(outputSchema ? { outputSchema } : {}) },
159
- collect: { ...step.collect, ...(collectSchema ? { outputSchema: collectSchema } : {}) },
160
- };
161
- }
162
- const outputSchema = loadSavedOutputSchema(chain, step.agent, (step as { outputSchema?: unknown }).outputSchema);
163
- return {
164
- agent: step.agent,
165
- task: step.task || undefined,
166
- ...(step.phase ? { phase: step.phase } : {}),
167
- ...(step.label ? { label: step.label } : {}),
168
- ...(step.as ? { as: step.as } : {}),
169
- ...(outputSchema ? { outputSchema } : {}),
170
- output: step.output,
171
- outputMode: step.outputMode,
172
- reads: step.reads,
173
- progress: step.progress,
174
- skill: step.skill ?? step.skills,
175
- model: step.model,
176
- };
177
- });
178
- };
179
-
180
126
  async function requestSlashRun(
181
127
  pi: ExtensionAPI,
182
128
  ctx: ExtensionContext,
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Changed
8
+
9
+ - Published a synchronized Atomic 0.8.29-alpha.1 prerelease with the upstream pi TUI dependency aligned to `^0.79.3`; no functional changes were made in the web-access extension.
10
+
7
11
  ## [0.8.28] - 2026-06-11
8
12
 
9
13
  ### Changed
@@ -434,7 +438,7 @@ All notable changes to this project will be documented in this file.
434
438
  ## [0.5.0] - 2026-02-01
435
439
 
436
440
  ### Added
437
- - GitHub repository clone extraction for `fetch_content` -- detects GitHub code URLs, clones repos to `/tmp/pi-github-repos/`, and returns actual file contents plus local path for further exploration with `read` and `bash`
441
+ - GitHub repository clone extraction for `fetch_content` -- detects GitHub code URLs, clones repos to `/tmp/atomic-github-repos/`, and returns actual file contents plus local path for further exploration with `read` and `bash`
438
442
  - Lightweight API fallback for oversized repos (>350MB) and commit SHA URLs via `gh api`
439
443
  - Clone cache with concurrent request deduplication (second request awaits first's clone)
440
444
  - `forceClone` parameter on `fetch_content` to override the size threshold
@@ -265,7 +265,7 @@ All config lives in `~/.pi/web-search.json`. Every field is optional.
265
265
  "enabled": true,
266
266
  "maxRepoSizeMB": 350,
267
267
  "cloneTimeoutSeconds": 30,
268
- "clonePath": "/tmp/pi-github-repos"
268
+ "clonePath": "/tmp/atomic-github-repos"
269
269
  },
270
270
  "youtube": {
271
271
  "enabled": true,