deepline 0.1.33 → 0.1.36

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.
@@ -67,6 +67,8 @@ import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-mani
67
67
 
68
68
  const TERMINAL_PLAY_STATUSES = new Set(['completed', 'failed', 'cancelled']);
69
69
  const INCLUDE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
70
+ const EXECUTE_RESPONSE_CONTRACT_HEADER = 'x-deepline-execute-response-contract';
71
+ const V2_EXECUTE_RESPONSE_CONTRACT = 'v2-tool-response';
70
72
  const COMPILE_MANIFEST_RETRY_DELAYS_MS = [250, 1_000];
71
73
 
72
74
  function sleep(ms: number): Promise<void> {
@@ -95,10 +97,13 @@ type ExecuteToolRawOptions = {
95
97
  export type ToolExecution<TData = unknown, TMeta = Record<string, unknown>> = {
96
98
  status: string;
97
99
  job_id?: string;
98
- result: {
99
- data: TData;
100
+ meta?: Record<string, unknown>;
101
+ toolResponse: {
102
+ raw: TData;
100
103
  meta?: TMeta;
101
104
  };
105
+ extractedLists?: Record<string, unknown>;
106
+ extractedValues?: Record<string, unknown>;
102
107
  billing?: Record<string, unknown>;
103
108
  [key: string]: unknown;
104
109
  };
@@ -552,19 +557,22 @@ export class DeeplineClient {
552
557
  /**
553
558
  * Execute a tool and return the standard execution envelope.
554
559
  *
555
- * The `result.data` field contains the provider payload. `result.meta`
556
- * contains provider/upstream metadata such as HTTP status or paging details.
560
+ * The `toolResponse.raw` field contains the raw tool response.
561
+ * `toolResponse.meta` contains tool/provider metadata.
557
562
  * Top-level fields such as `status`, `job_id`, and `billing` describe the
558
- * Deepline execution.
563
+ * Deepline execution envelope.
559
564
  */
560
565
  async executeTool<TData = unknown, TMeta = Record<string, unknown>>(
561
566
  toolId: string,
562
567
  input: Record<string, unknown>,
563
568
  options?: ExecuteToolRawOptions,
564
569
  ): Promise<ToolExecution<TData, TMeta>> {
565
- const headers = options?.includeToolMetadata
566
- ? { [INCLUDE_TOOL_METADATA_HEADER]: 'true' }
567
- : undefined;
570
+ const headers = {
571
+ [EXECUTE_RESPONSE_CONTRACT_HEADER]: V2_EXECUTE_RESPONSE_CONTRACT,
572
+ ...(options?.includeToolMetadata
573
+ ? { [INCLUDE_TOOL_METADATA_HEADER]: 'true' }
574
+ : {}),
575
+ };
568
576
  return this.http.post<ToolExecution<TData, TMeta>>(
569
577
  `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
570
578
  { payload: input },
@@ -1029,34 +1037,37 @@ export class DeeplineClient {
1029
1037
  bytes: number;
1030
1038
  }>,
1031
1039
  ): Promise<PlayStagedFileRef[]> {
1032
- const formData = new FormData();
1033
- formData.set(
1034
- 'metadata',
1035
- JSON.stringify({
1036
- files: files.map((file, index) => ({
1037
- index,
1038
- logicalPath: file.logicalPath,
1039
- contentHash: file.contentHash,
1040
- contentType: file.contentType,
1041
- bytes: file.bytes,
1042
- })),
1043
- }),
1044
- );
1045
- for (const [index, file] of files.entries()) {
1046
- const bytes = decodeBase64Bytes(file.contentBase64);
1047
- const body = bytes.buffer.slice(
1048
- bytes.byteOffset,
1049
- bytes.byteOffset + bytes.byteLength,
1050
- ) as ArrayBuffer;
1040
+ const buildFormData = () => {
1041
+ const formData = new FormData();
1051
1042
  formData.set(
1052
- `file:${index}`,
1053
- new Blob([body], { type: file.contentType }),
1054
- file.logicalPath,
1043
+ 'metadata',
1044
+ JSON.stringify({
1045
+ files: files.map((file, index) => ({
1046
+ index,
1047
+ logicalPath: file.logicalPath,
1048
+ contentHash: file.contentHash,
1049
+ contentType: file.contentType,
1050
+ bytes: file.bytes,
1051
+ })),
1052
+ }),
1055
1053
  );
1056
- }
1054
+ for (const [index, file] of files.entries()) {
1055
+ const bytes = decodeBase64Bytes(file.contentBase64);
1056
+ const body = bytes.buffer.slice(
1057
+ bytes.byteOffset,
1058
+ bytes.byteOffset + bytes.byteLength,
1059
+ ) as ArrayBuffer;
1060
+ formData.set(
1061
+ `file:${index}`,
1062
+ new Blob([body], { type: file.contentType }),
1063
+ file.logicalPath,
1064
+ );
1065
+ }
1066
+ return formData;
1067
+ };
1057
1068
  const response = await this.http.postFormData<{ files: PlayStagedFileRef[] }>(
1058
1069
  '/api/v2/plays/files/stage',
1059
- formData,
1070
+ buildFormData,
1060
1071
  );
1061
1072
  return response.files;
1062
1073
  }
@@ -31,7 +31,7 @@ import {
31
31
  interface RequestOptions {
32
32
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
33
33
  body?: unknown;
34
- formData?: FormData;
34
+ formData?: FormData | (() => FormData);
35
35
  headers?: Record<string, string>;
36
36
  /** Per-request timeout override in milliseconds. */
37
37
  timeout?: number;
@@ -153,7 +153,9 @@ export class HttpClient {
153
153
  headers,
154
154
  body:
155
155
  options?.formData !== undefined
156
- ? options.formData
156
+ ? typeof options.formData === 'function'
157
+ ? options.formData()
158
+ : options.formData
157
159
  : options?.body !== undefined
158
160
  ? JSON.stringify(options.body)
159
161
  : undefined,
@@ -277,10 +279,13 @@ export class HttpClient {
277
279
  throw new AuthError();
278
280
  }
279
281
  if (!response.ok) {
282
+ const body = await response.text();
283
+ const parsed = parseResponseBody(body);
280
284
  throw new DeeplineError(
281
- `HTTP ${response.status}`,
285
+ apiErrorMessage(parsed, response.status),
282
286
  response.status,
283
287
  'API_ERROR',
288
+ { response: parsed },
284
289
  );
285
290
  }
286
291
  if (!response.body) {
@@ -325,7 +330,7 @@ export class HttpClient {
325
330
 
326
331
  async postFormData<T = unknown>(
327
332
  path: string,
328
- formData: FormData,
333
+ formData: FormData | (() => FormData),
329
334
  headers?: Record<string, string>,
330
335
  ): Promise<T> {
331
336
  return this.request<T>(path, {
@@ -346,6 +351,41 @@ export class HttpClient {
346
351
  }
347
352
  }
348
353
 
354
+ function parseResponseBody(body: string): unknown {
355
+ try {
356
+ return JSON.parse(body);
357
+ } catch {
358
+ return body;
359
+ }
360
+ }
361
+
362
+ function apiErrorMessage(parsed: unknown, status: number): string {
363
+ const errorValue =
364
+ typeof parsed === 'object' && parsed && 'error' in parsed
365
+ ? (parsed as Record<string, unknown>).error
366
+ : undefined;
367
+ if (typeof errorValue === 'string') {
368
+ return errorValue;
369
+ }
370
+ if (
371
+ errorValue &&
372
+ typeof errorValue === 'object' &&
373
+ 'message' in errorValue &&
374
+ typeof (errorValue as Record<string, unknown>).message === 'string'
375
+ ) {
376
+ return (errorValue as Record<string, string>).message;
377
+ }
378
+ if (
379
+ typeof parsed === 'object' &&
380
+ parsed &&
381
+ 'message' in parsed &&
382
+ typeof (parsed as Record<string, unknown>).message === 'string'
383
+ ) {
384
+ return (parsed as Record<string, string>).message;
385
+ }
386
+ return `HTTP ${status}`;
387
+ }
388
+
349
389
  /** Parse the `Retry-After` header as milliseconds. Falls back to 5000ms. */
350
390
  function parseRetryAfter(response: Response): number {
351
391
  const header = response.headers.get('retry-after');
@@ -7,14 +7,14 @@
7
7
  * import { Deepline, definePlay } from 'deepline';
8
8
  *
9
9
  * // Connect (auto-resolves API key from env / CLI config)
10
- * const ctx = await Deepline.connect();
10
+ * const deepline = await Deepline.connect();
11
11
  *
12
12
  * // Execute a tool
13
- * const result = await ctx.tools.execute('test_company_search', { domain: 'stripe.com' });
14
- * const company = result.result.data;
13
+ * const result = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' });
14
+ * const company = result.toolResponse.raw;
15
15
  *
16
16
  * // Run a named play
17
- * const job = await ctx.play('email-waterfall').run({ domain: 'stripe.com' });
17
+ * const job = await deepline.play('email-waterfall').run({ domain: 'stripe.com' });
18
18
  * const result = await job.get();
19
19
  * ```
20
20
  *
@@ -25,7 +25,10 @@
25
25
  *
26
26
  * export default definePlay('my-play', async (ctx, input: { domain: string }) => {
27
27
  * ctx.log(`Looking up ${input.domain}`);
28
- * const company = await ctx.tools.execute('company_search', 'test_company_search', { domain: input.domain }, {
28
+ * const company = await ctx.tools.execute({
29
+ * id: 'company_search',
30
+ * tool: 'test_company_search',
31
+ * input: { domain: input.domain },
29
32
  * description: 'Look up company details by domain.',
30
33
  * });
31
34
  * return company;
@@ -28,7 +28,10 @@
28
28
  *
29
29
  * export default definePlay('my-play', async (ctx, input: { domain: string }) => {
30
30
  * ctx.log(`Looking up ${input.domain}`);
31
- * const company = await ctx.tools.execute('company_search', 'test_company_search', { domain: input.domain }, {
31
+ * const company = await ctx.tools.execute({
32
+ * id: 'company_search',
33
+ * tool: 'test_company_search',
34
+ * input: { domain: input.domain },
32
35
  * description: 'Look up company details by domain.',
33
36
  * });
34
37
  * return company;
@@ -42,8 +45,8 @@
42
45
  * ```typescript
43
46
  * import { Deepline } from 'deepline';
44
47
  *
45
- * const ctx = await Deepline.connect();
46
- * const job = await ctx.play('my-play').run({ domain: 'stripe.com' });
48
+ * const deepline = await Deepline.connect();
49
+ * const job = await deepline.play('my-play').run({ domain: 'stripe.com' });
47
50
  * const result = await job.get(); // Polls until complete
48
51
  * ```
49
52
  *
@@ -53,7 +56,10 @@
53
56
  * import { definePlay } from 'deepline';
54
57
  *
55
58
  * export default definePlay('daily-sync', async (ctx) => {
56
- * const data = await ctx.tools.execute('crm_export', 'crm_export', {}, {
59
+ * const data = await ctx.tools.execute({
60
+ * id: 'crm_export',
61
+ * tool: 'crm_export',
62
+ * input: {},
57
63
  * description: 'Export CRM records for the daily sync.',
58
64
  * });
59
65
  * return data;
@@ -66,6 +72,7 @@
66
72
  */
67
73
  import { DeeplineClient } from './client.js';
68
74
  import { DeeplineError } from './errors.js';
75
+ import { createToolExecuteResult } from '../../shared_libs/play-runtime/tool-result.js';
69
76
  import type {
70
77
  PlayDataset,
71
78
  PlayDatasetInput,
@@ -83,6 +90,7 @@ import type {
83
90
  ToolDefinition,
84
91
  ToolMetadata,
85
92
  } from './types.js';
93
+ import type { ToolExecution } from './client.js';
86
94
 
87
95
  /**
88
96
  * Optional trigger bindings for a play.
@@ -275,7 +283,10 @@ export type CsvOptions = {
275
283
  * ```typescript
276
284
  * definePlay('example', async (ctx, input: { domain: string }) => {
277
285
  * // Call a tool
278
- * const company = await ctx.tools.execute('company_search', 'test_company_search', { domain: input.domain }, {
286
+ * const company = await ctx.tools.execute({
287
+ * id: 'company_search',
288
+ * tool: 'test_company_search',
289
+ * input: { domain: input.domain },
279
290
  * description: 'Look up company details by domain.',
280
291
  * });
281
292
  *
@@ -348,24 +359,30 @@ export interface DeeplinePlayRuntimeContext {
348
359
  *
349
360
  * @example Single tool per row
350
361
  * ```typescript
351
- * const results = await ctx
352
- * .map('companies', leads)
353
- * .step('company', (row, ctx) =>
354
- * ctx.tools.execute('company_search', 'test_company_search', { domain: row.domain }, {
355
- * description: 'Look up company details by domain.',
356
- * }))
362
+ * const results = await ctx
363
+ * .map('companies', leads)
364
+ * .step('company', (row, ctx) =>
365
+ * ctx.tools.execute({
366
+ * id: 'company_search',
367
+ * tool: 'test_company_search',
368
+ * input: { domain: row.domain },
369
+ * description: 'Look up company details by domain.',
370
+ * }))
357
371
  * .run({ description: 'Look up companies.' });
358
372
  * // [{ domain: 'stripe.com', company: { name: 'Stripe', ... } }, ...]
359
373
  * ```
360
374
  *
361
375
  * @example Multiple columns with pre/post logic
362
376
  * ```typescript
363
- * const results = await ctx
364
- * .map('leads', leads)
365
- * .step('company', (row, ctx) =>
366
- * ctx.tools.execute('company_search', 'test_company_search', { domain: row.domain }, {
367
- * description: 'Look up company details by domain.',
368
- * }))
377
+ * const results = await ctx
378
+ * .map('leads', leads)
379
+ * .step('company', (row, ctx) =>
380
+ * ctx.tools.execute({
381
+ * id: 'company_search',
382
+ * tool: 'test_company_search',
383
+ * input: { domain: row.domain },
384
+ * description: 'Look up company details by domain.',
385
+ * }))
369
386
  * .step('score', (row) =>
370
387
  * row.company?.employeeCount > 100 ? 'enterprise' : 'smb')
371
388
  * .run({ description: 'Enrich leads.' });
@@ -379,22 +396,16 @@ export interface DeeplinePlayRuntimeContext {
379
396
  /** Tool execution namespace. */
380
397
  tools: {
381
398
  /**
382
- * Execute a single tool by stable step key and tool ID.
399
+ * Execute a single tool with a keyword-style request object.
383
400
  *
384
- * @param key - Stable step key for idempotent execution
385
- * @param toolId - Tool identifier (e.g. `'test_company_search'`)
386
- * @param input - Tool-specific input parameters
401
+ * @param request.id - Stable step key for idempotent execution
402
+ * @param request.tool - Tool identifier (e.g. `'test_company_search'`)
403
+ * @param request.input - Tool-specific input parameters
387
404
  * @returns The tool's output
388
405
  */
389
406
  execute<TOutput = LoosePlayObject>(
390
407
  request: ToolExecutionRequest,
391
408
  ): Promise<ToolExecuteResult<TOutput>>;
392
- execute<TOutput = LoosePlayObject>(
393
- key: string,
394
- toolId: string,
395
- input: Record<string, unknown>,
396
- options?: { description?: string },
397
- ): Promise<ToolExecuteResult<TOutput>>;
398
409
  };
399
410
  /**
400
411
  * Execute a single tool by stable step key and tool ID.
@@ -736,7 +747,10 @@ export function when<Row, Value>(
736
747
  * import { definePlay } from 'deepline';
737
748
  *
738
749
  * const myPlay = definePlay('my-play', async (ctx, input: { domain: string }) => {
739
- * return await ctx.tools.execute('company_search', 'test_company_search', { domain: input.domain }, {
750
+ * return await ctx.tools.execute({
751
+ * id: 'company_search',
752
+ * tool: 'test_company_search',
753
+ * input: { domain: input.domain },
740
754
  * description: 'Look up company details by domain.',
741
755
  * });
742
756
  * });
@@ -886,14 +900,14 @@ function createNamedPlayHandle<
886
900
  *
887
901
  * @example
888
902
  * ```typescript
889
- * const ctx = await Deepline.connect();
903
+ * const deepline = await Deepline.connect();
890
904
  *
891
905
  * // Tools
892
- * const tools = await ctx.tools.list();
893
- * const result = await ctx.tools.execute('test_company_search', { domain: 'stripe.com' });
906
+ * const tools = await deepline.tools.list();
907
+ * const result = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' });
894
908
  *
895
909
  * // Plays
896
- * const job = await ctx.play('email-waterfall').run({ domain: 'stripe.com' });
910
+ * const job = await deepline.play('email-waterfall').run({ domain: 'stripe.com' });
897
911
  * const output = await job.get();
898
912
  * ```
899
913
  */
@@ -909,10 +923,10 @@ export class DeeplineContext {
909
923
  *
910
924
  * @example
911
925
  * ```typescript
912
- * const tools = await ctx.tools.list();
913
- * const meta = await ctx.tools.get('apollo_people_search');
914
- * const companyLookup = await ctx.tools.execute('test_company_search', { domain: 'stripe.com' });
915
- * const company = companyLookup.result.data;
926
+ * const tools = await deepline.tools.list();
927
+ * const meta = await deepline.tools.get('apollo_people_search');
928
+ * const companyLookup = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' });
929
+ * const company = companyLookup.toolResponse.raw;
916
930
  * ```
917
931
  */
918
932
  get tools() {
@@ -926,12 +940,14 @@ export class DeeplineContext {
926
940
  execute: async (
927
941
  toolId: string,
928
942
  input: Record<string, unknown>,
929
- ): Promise<ToolExecuteResult> =>
930
- this.client.executeTool(
943
+ ): Promise<ToolExecuteResult> => {
944
+ const response = await this.client.executeTool(
931
945
  toolId,
932
946
  input,
933
947
  { includeToolMetadata: true },
934
- ) as unknown as Promise<ToolExecuteResult>,
948
+ );
949
+ return toolExecutionEnvelopeToResult(toolId, response);
950
+ },
935
951
  };
936
952
  }
937
953
 
@@ -1023,9 +1039,9 @@ export class DeeplineContext {
1023
1039
  * ```typescript
1024
1040
  * import { Deepline } from 'deepline';
1025
1041
  *
1026
- * const ctx = await Deepline.connect();
1027
- * const tools = await ctx.tools.list();
1028
- * const result = await ctx.tools.execute('test_company_search', { domain: 'stripe.com' });
1042
+ * const deepline = await Deepline.connect();
1043
+ * const tools = await deepline.tools.list();
1044
+ * const result = await deepline.tools.execute('test_company_search', { domain: 'stripe.com' });
1029
1045
  * ```
1030
1046
  */
1031
1047
  export class Deepline {
@@ -1058,6 +1074,62 @@ export class Deepline {
1058
1074
  }
1059
1075
  }
1060
1076
 
1077
+ function isRecord(value: unknown): value is Record<string, unknown> {
1078
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1079
+ }
1080
+
1081
+ function stringArrayRecord(value: unknown): Record<string, string[]> {
1082
+ if (!isRecord(value)) return {};
1083
+ return Object.fromEntries(
1084
+ Object.entries(value).map(([key, paths]) => [
1085
+ key,
1086
+ Array.isArray(paths) ? paths.map(String) : [],
1087
+ ]),
1088
+ );
1089
+ }
1090
+
1091
+ function stringArray(value: unknown): string[] {
1092
+ return Array.isArray(value) ? value.map(String) : [];
1093
+ }
1094
+
1095
+ function toolExecutionEnvelopeToResult(
1096
+ fallbackToolId: string,
1097
+ response: ToolExecution,
1098
+ ): ToolExecuteResult {
1099
+ const raw = response.toolResponse?.raw ?? null;
1100
+ const meta = response.toolResponse?.meta;
1101
+ const metadata = isRecord(response._metadata)
1102
+ ? response._metadata.tool
1103
+ : null;
1104
+ const toolMetadata = isRecord(metadata) ? metadata : {};
1105
+
1106
+ return createToolExecuteResult({
1107
+ status: typeof response.status === 'string' ? response.status : 'completed',
1108
+ jobId: typeof response.job_id === 'string' ? response.job_id : undefined,
1109
+ result: {
1110
+ data: raw,
1111
+ ...(isRecord(meta) ? { meta } : {}),
1112
+ },
1113
+ metadata: {
1114
+ toolId:
1115
+ typeof toolMetadata.toolId === 'string'
1116
+ ? toolMetadata.toolId
1117
+ : fallbackToolId,
1118
+ resultIdentityGetters: stringArrayRecord(
1119
+ toolMetadata.resultIdentityGetters,
1120
+ ),
1121
+ listExtractorPaths: stringArray(toolMetadata.listExtractorPaths),
1122
+ listIdentityGetters: stringArrayRecord(toolMetadata.listIdentityGetters),
1123
+ },
1124
+ execution: {
1125
+ idempotent: true,
1126
+ cached: false,
1127
+ source: 'live',
1128
+ },
1129
+ meta: isRecord(response.meta) ? response.meta : undefined,
1130
+ });
1131
+ }
1132
+
1061
1133
  export function defineInput<TInput>(
1062
1134
  schema: Record<string, unknown>,
1063
1135
  ): PlayInputContract<TInput> {
@@ -1092,7 +1164,10 @@ export function defineInput<TInput>(
1092
1164
  *
1093
1165
  * export default definePlay('company-lookup', async (ctx, input: { domain: string }) => {
1094
1166
  * ctx.log(`Searching for ${input.domain}`);
1095
- * const company = await ctx.tools.execute('company_search', 'test_company_search', { domain: input.domain }, {
1167
+ * const company = await ctx.tools.execute({
1168
+ * id: 'company_search',
1169
+ * tool: 'test_company_search',
1170
+ * input: { domain: input.domain },
1096
1171
  * description: 'Look up company details by domain.',
1097
1172
  * });
1098
1173
  * return company;
@@ -1107,7 +1182,10 @@ export function defineInput<TInput>(
1107
1182
  * const results = await ctx
1108
1183
  * .map('companies', leads)
1109
1184
  * .step('company', (row, ctx) =>
1110
- * ctx.tools.execute('company_search', 'test_company_search', { domain: row.domain }, {
1185
+ * ctx.tools.execute({
1186
+ * id: 'company_search',
1187
+ * tool: 'test_company_search',
1188
+ * input: { domain: row.domain },
1111
1189
  * description: 'Look up company details by domain.',
1112
1190
  * }))
1113
1191
  * .run({ description: 'Enrich lead companies.' });
@@ -1118,7 +1196,10 @@ export function defineInput<TInput>(
1118
1196
  * @example With cron binding
1119
1197
  * ```typescript
1120
1198
  * export default definePlay('daily-report', async (ctx) => {
1121
- * const data = await ctx.tools.execute('crm_export', 'crm_export', { since: 'yesterday' }, {
1199
+ * const data = await ctx.tools.execute({
1200
+ * id: 'crm_export',
1201
+ * tool: 'crm_export',
1202
+ * input: { since: 'yesterday' },
1122
1203
  * description: 'Export yesterday CRM records.',
1123
1204
  * });
1124
1205
  * return data;
@@ -155,6 +155,7 @@ export async function harnessPrewarmPostgresSessions(input: {
155
155
  export async function harnessStartSheetDataset(input: {
156
156
  baseUrl: string;
157
157
  executorToken: string;
158
+ preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
158
159
  playName: string;
159
160
  tableNamespace: string;
160
161
  sheetContract: unknown;
@@ -162,7 +163,6 @@ export async function harnessStartSheetDataset(input: {
162
163
  runId: string;
163
164
  inputOffset?: number;
164
165
  userEmail?: string | null;
165
- preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
166
166
  }): Promise<{
167
167
  inserted: number;
168
168
  skipped: number;
@@ -181,6 +181,7 @@ export async function harnessStartSheetDataset(input: {
181
181
  export async function harnessPersistCompletedSheetRows(input: {
182
182
  baseUrl: string;
183
183
  executorToken: string;
184
+ preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
184
185
  playName: string;
185
186
  tableNamespace: string;
186
187
  sheetContract: unknown;
@@ -188,7 +189,6 @@ export async function harnessPersistCompletedSheetRows(input: {
188
189
  outputFields: string[];
189
190
  runId: string;
190
191
  userEmail?: string | null;
191
- preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
192
192
  }): Promise<{ ok: true; rowsWritten: number; tableNamespace: string }> {
193
193
  return requireBinding().persistCompletedMapRows(input);
194
194
  }
@@ -2,7 +2,7 @@
2
2
  * Tool output processing utilities.
3
3
  *
4
4
  * Tools return data in varied shapes — some return flat objects, others
5
- * wrap results in `{ result: { data: [...] } }` envelopes, and list
5
+ * wrap results in `{ output: { body: [...] } }` envelopes, and list
6
6
  * responses can be nested at different depths. This module provides
7
7
  * utilities to normalize, extract, and persist tool outputs.
8
8
  *
@@ -33,7 +33,7 @@ import { join } from 'node:path';
33
33
  * @example
34
34
  * ```typescript
35
35
  * const conversion = tryConvertToList(toolResponse, {
36
- * listExtractorPaths: ['people', 'result.data'],
36
+ * listExtractorPaths: ['people', 'output.body'],
37
37
  * });
38
38
  * if (conversion) {
39
39
  * console.log(`Found ${conversion.rows.length} rows via ${conversion.strategy}`);
@@ -50,7 +50,7 @@ export type ListConversionResult = {
50
50
  * - `'auto_detected'` — found via recursive DFS (longest array wins)
51
51
  */
52
52
  strategy: 'configured_paths' | 'auto_detected';
53
- /** Dotted path to where the list was found (e.g. `"result.data"`, `"people"`). */
53
+ /** Dotted path to where the list was found (e.g. `"output.body"`, `"people"`). */
54
54
  sourcePath: string | null;
55
55
  };
56
56
 
@@ -75,7 +75,7 @@ function normalizeScalarString(value: unknown): string | null {
75
75
  * Traverse a nested object by a dotted path string.
76
76
  *
77
77
  * @param root - Object to traverse
78
- * @param dottedPath - Path like `"result.data.items"`
78
+ * @param dottedPath - Path like `"output.body.items"`
79
79
  * @returns Value at the path, or `null` if not found
80
80
  *
81
81
  * @example
@@ -109,10 +109,25 @@ function normalizeRows(value: unknown): Array<Record<string, unknown>> | null {
109
109
 
110
110
  /**
111
111
  * Generate candidate root objects to search for lists.
112
- * Tries: raw payload → payload.result → payload.result.data.
112
+ * Tries: raw payload → V2 toolResponse.raw → legacy payload.output.body → legacy payload.result → legacy payload.result.data.
113
113
  */
114
114
  function candidateRoots(payload: unknown): Array<{ path: string | null; value: unknown }> {
115
115
  const roots: Array<{ path: string | null; value: unknown }> = [{ path: null, value: payload }];
116
+ if (isPlainObject(payload) && isPlainObject(payload.toolResponse)) {
117
+ roots.push({ path: 'toolResponse', value: payload.toolResponse });
118
+ if (Object.prototype.hasOwnProperty.call(payload.toolResponse, 'raw')) {
119
+ roots.push({
120
+ path: 'toolResponse.raw',
121
+ value: payload.toolResponse.raw,
122
+ });
123
+ }
124
+ }
125
+ if (isPlainObject(payload) && isPlainObject(payload.output)) {
126
+ roots.push({ path: 'output', value: payload.output });
127
+ if (Object.prototype.hasOwnProperty.call(payload.output, 'body')) {
128
+ roots.push({ path: 'output.body', value: payload.output.body });
129
+ }
130
+ }
116
131
  if (isPlainObject(payload) && isPlainObject(payload.result)) {
117
132
  roots.push({ path: 'result', value: payload.result });
118
133
  if (isPlainObject(payload.result.data)) {
@@ -167,7 +182,7 @@ function findBestArrayCandidate(
167
182
  * ## Extraction strategy
168
183
  *
169
184
  * 1. **Configured paths** — If `listExtractorPaths` is provided, each path is
170
- * tried against multiple candidate roots (raw payload, `.result`, `.result.data`).
185
+ * tried against multiple candidate roots (raw payload, `.output.body`, legacy `.result`, legacy `.result.data`).
171
186
  * First match wins.
172
187
  *
173
188
  * 2. **Auto-detection** — If no configured path matches, recursively searches
@@ -344,7 +359,7 @@ export function writeCsvOutputFile(
344
359
  /**
345
360
  * Extract scalar (non-nested) fields from a tool response for summary display.
346
361
  *
347
- * Searches through candidate roots (raw → `.result` → `.result.data`) and
362
+ * Searches through candidate roots (raw → `.output.body` → legacy `.result` → legacy `.result.data`) and
348
363
  * returns the first set of scalar fields found. Useful for displaying a
349
364
  * quick summary of single-record responses.
350
365
  *