@inflexa-ai/harness 0.45.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/agents/conversation-agent.d.ts +10 -0
  2. package/dist/agents/conversation-agent.js +19 -1
  3. package/dist/agents/sandbox/shared.d.ts +23 -2
  4. package/dist/agents/sandbox/shared.js +51 -18
  5. package/dist/agents/sandbox/translational-safety-agent.js +26 -0
  6. package/dist/agents/sandbox/types.d.ts +24 -1
  7. package/dist/agents/sandbox-catalog.d.ts +14 -0
  8. package/dist/agents/sandbox-catalog.js +19 -0
  9. package/dist/app/approve-plan.d.ts +5 -2
  10. package/dist/app/approve-plan.js +1 -1
  11. package/dist/app/chat-turn.js +6 -3
  12. package/dist/config/environment-stores.d.ts +4 -2
  13. package/dist/contracts/chat-events.d.ts +12 -2
  14. package/dist/contracts/chat-events.js +1 -1
  15. package/dist/contracts/chat-frame.js +6 -2
  16. package/dist/contracts/index.d.ts +1 -1
  17. package/dist/contracts/schemas/chat-events.d.ts +25 -0
  18. package/dist/contracts/schemas/chat-events.js +7 -0
  19. package/dist/contracts/schemas/chat-parts.d.ts +39 -0
  20. package/dist/contracts/schemas/chat-parts.js +17 -0
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/dbos-workflow-purger.d.ts +2 -0
  24. package/dist/execution/dbos-workflow-purger.js +21 -14
  25. package/dist/execution/run-canceler.d.ts +33 -11
  26. package/dist/execution/run-canceler.js +129 -27
  27. package/dist/execution/run-end-record.d.ts +30 -0
  28. package/dist/execution/run-end-record.js +27 -0
  29. package/dist/index.d.ts +9 -5
  30. package/dist/index.js +11 -1
  31. package/dist/loop/call-guard.d.ts +50 -0
  32. package/dist/loop/call-guard.js +90 -0
  33. package/dist/loop/compaction.d.ts +5 -0
  34. package/dist/loop/compaction.js +17 -0
  35. package/dist/loop/run-agent.d.ts +9 -0
  36. package/dist/loop/run-agent.js +15 -1
  37. package/dist/loop/run-to-terminal.js +4 -1
  38. package/dist/loop/types.d.ts +6 -0
  39. package/dist/memory/conversation-display-recorder.d.ts +0 -2
  40. package/dist/memory/conversation-display-recorder.js +28 -3
  41. package/dist/memory/thread-history.d.ts +34 -7
  42. package/dist/memory/thread-history.js +151 -42
  43. package/dist/prompts/briefing.d.ts +57 -3
  44. package/dist/prompts/briefing.js +129 -2
  45. package/dist/prompts/planner.js +13 -2
  46. package/dist/provenance/seam.d.ts +23 -15
  47. package/dist/provenance/seam.js +7 -5
  48. package/dist/providers/ai-sdk.js +87 -7
  49. package/dist/providers/types.d.ts +6 -0
  50. package/dist/reference-data/catalog.js +141 -2
  51. package/dist/report-model/authoring-grammar.d.ts +26 -26
  52. package/dist/sandbox/image-packages.d.ts +26 -4
  53. package/dist/sandbox/image-packages.js +18 -4
  54. package/dist/schemas/data-profile-schemas.d.ts +12 -12
  55. package/dist/schemas/plan-schemas.d.ts +57 -3
  56. package/dist/schemas/plan-schemas.js +1 -1
  57. package/dist/schemas/workflow-state.d.ts +113 -0
  58. package/dist/schemas/workflow-state.js +54 -0
  59. package/dist/state/artifacts.d.ts +17 -1
  60. package/dist/state/artifacts.js +21 -3
  61. package/dist/state/index.d.ts +2 -2
  62. package/dist/state/index.js +1 -1
  63. package/dist/state/runs.d.ts +43 -9
  64. package/dist/state/runs.js +85 -14
  65. package/dist/state/step-executions.d.ts +9 -0
  66. package/dist/state/step-executions.js +22 -0
  67. package/dist/tools/ad-hoc-router.d.ts +2 -0
  68. package/dist/tools/ad-hoc-router.js +18 -2
  69. package/dist/tools/capability-data.d.ts +36 -0
  70. package/dist/tools/capability-data.js +73 -0
  71. package/dist/tools/execute-analysis.d.ts +37 -9
  72. package/dist/tools/execute-analysis.js +131 -21
  73. package/dist/tools/knowledge/check.d.ts +60 -0
  74. package/dist/tools/knowledge/check.js +92 -0
  75. package/dist/tools/knowledge/client.d.ts +382 -0
  76. package/dist/tools/knowledge/client.js +302 -0
  77. package/dist/tools/knowledge/environment.d.ts +78 -0
  78. package/dist/tools/knowledge/environment.js +105 -0
  79. package/dist/tools/knowledge/index.d.ts +49 -0
  80. package/dist/tools/knowledge/index.js +70 -0
  81. package/dist/tools/knowledge/local-slots.d.ts +62 -0
  82. package/dist/tools/knowledge/local-slots.js +177 -0
  83. package/dist/tools/knowledge/pin.d.ts +14 -0
  84. package/dist/tools/knowledge/pin.js +17 -0
  85. package/dist/tools/knowledge/recommend.d.ts +92 -0
  86. package/dist/tools/knowledge/recommend.js +98 -0
  87. package/dist/tools/knowledge/situation.d.ts +143 -0
  88. package/dist/tools/knowledge/situation.js +164 -0
  89. package/dist/tools/knowledge/skeleton.d.ts +69 -0
  90. package/dist/tools/knowledge/skeleton.js +202 -0
  91. package/dist/tools/knowledge/template.d.ts +137 -0
  92. package/dist/tools/knowledge/template.js +267 -0
  93. package/dist/tools/research/generate-plan.d.ts +23 -0
  94. package/dist/tools/research/generate-plan.js +203 -12
  95. package/dist/tools/research/guarded-address.d.ts +20 -0
  96. package/dist/tools/research/guarded-address.js +148 -0
  97. package/dist/tools/research/searxng-engine.d.ts +40 -0
  98. package/dist/tools/research/searxng-engine.js +120 -0
  99. package/dist/tools/research/web-fetch.d.ts +124 -0
  100. package/dist/tools/research/web-fetch.js +627 -0
  101. package/dist/tools/research/web-search.d.ts +75 -0
  102. package/dist/tools/research/web-search.js +112 -0
  103. package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
  104. package/dist/tools/sandbox/list-available-packages.js +12 -4
  105. package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
  106. package/dist/tools/sandbox/list-available-refs.js +60 -3
  107. package/dist/tools/sandbox/report-blocker.js +5 -1
  108. package/dist/tools/terminal-text.d.ts +11 -0
  109. package/dist/tools/terminal-text.js +19 -0
  110. package/dist/tools/workspace/decision-record.d.ts +81 -0
  111. package/dist/tools/workspace/decision-record.js +117 -0
  112. package/dist/tools/workspace/edit-file.d.ts +3 -1
  113. package/dist/tools/workspace/edit-file.js +20 -2
  114. package/dist/tools/workspace/execute-command.d.ts +17 -0
  115. package/dist/tools/workspace/execute-command.js +28 -1
  116. package/dist/tools/workspace/index.d.ts +1 -0
  117. package/dist/tools/workspace/index.js +1 -0
  118. package/dist/tools/workspace/mutator.d.ts +1 -1
  119. package/dist/workflows/execute-analysis.d.ts +58 -18
  120. package/dist/workflows/execute-analysis.js +273 -58
  121. package/dist/workflows/sandbox-step.d.ts +12 -0
  122. package/package.json +5 -1
@@ -9,6 +9,7 @@
9
9
  * `DbError`. Every other write maps SQLSTATE 23505/23503/23502/23514 to
10
10
  * `err(constraint_violation)` via `tryMutation`.
11
11
  */
12
+ import { stripNulCharacters } from "../input-sanitization.js";
12
13
  import { tryMutation, tryQuery } from "../lib/db-result.js";
13
14
  export class RunIdentityCollisionError extends Error {
14
15
  runId;
@@ -44,7 +45,7 @@ export class RunDedupCollisionError extends Error {
44
45
  * the err channel as a `DbError`.
45
46
  */
46
47
  export function insertRun(pool, input) {
47
- const now = new Date().toISOString();
48
+ const now = input.startedAt ?? new Date().toISOString();
48
49
  return tryMutation("runs.insertRun", async () => {
49
50
  await pool.query({
50
51
  text: `INSERT INTO cortex_runs
@@ -75,7 +76,7 @@ export function insertRun(pool, input) {
75
76
  }
76
77
  /** Reserve a deterministic run id, treating same-analysis PK conflict as redelivery. */
77
78
  export function reserveRunById(pool, input) {
78
- const now = new Date().toISOString();
79
+ const now = input.startedAt ?? new Date().toISOString();
79
80
  return tryMutation("runs.reserveRunById", async () => {
80
81
  const result = await pool.query({
81
82
  text: `INSERT INTO cortex_runs
@@ -139,6 +140,20 @@ export function queryActiveRun(pool, analysisId, planId) {
139
140
  return row ? mapRunRow(row) : null;
140
141
  });
141
142
  }
143
+ /**
144
+ * The SQL condition of the text filter `q` for the bound placeholder `placeholder`. A run matches when its id holds the
145
+ * text, or when the `title` of the plan of the same analysis holds it. `strpos` is the test, because `LIKE` would turn
146
+ * `%` and `_` of the input into wildcards. The `analysis_id` condition keeps the plan of a different analysis out, the
147
+ * same scope as `loadPlan`.
148
+ */
149
+ function runTextCondition(placeholder) {
150
+ return `(strpos(lower(run_id), lower(${placeholder}::text)) > 0
151
+ OR EXISTS (SELECT 1 FROM cortex_plans p
152
+ WHERE p.plan_id = cortex_runs.plan_id
153
+ AND p.analysis_id = cortex_runs.analysis_id
154
+ AND jsonb_typeof(p.plan -> 'title') = 'string'
155
+ AND strpos(lower(p.plan ->> 'title'), lower(${placeholder}::text)) > 0))`;
156
+ }
142
157
  /**
143
158
  * Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
144
159
  *
@@ -155,9 +170,13 @@ export function queryActiveRun(pool, analysisId, planId) {
155
170
  * The status set is the one {@link queryActiveRun} uses — non-terminal means `running`. Keep the two
156
171
  * in step: a new non-terminal status added to one and not the other makes a live run invisible to
157
172
  * exactly one of its readers.
173
+ *
174
+ * With `q`, only the runs that the text filter selects are members.
158
175
  */
159
- export function queryActiveRunsByAnalysis(pool, analysisId) {
176
+ export function queryActiveRunsByAnalysis(pool, analysisId, { q } = {}) {
160
177
  return tryQuery("runs.queryActiveRunsByAnalysis", async () => {
178
+ const values = [analysisId];
179
+ const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
161
180
  const result = await pool.query({
162
181
  text: `SELECT run_id, analysis_id, thread_id, workflow_name,
163
182
  status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
@@ -165,8 +184,9 @@ export function queryActiveRunsByAnalysis(pool, analysisId) {
165
184
  FROM cortex_runs
166
185
  WHERE analysis_id = $1
167
186
  AND status = 'running'
187
+ ${text}
168
188
  ORDER BY started_at DESC`,
169
- values: [analysisId],
189
+ values,
170
190
  });
171
191
  return result.rows.map(mapRunRow);
172
192
  });
@@ -207,16 +227,22 @@ export function reconcileOrphanedRuns(pool, opts = {}) {
207
227
  return result.rows.map((row) => row.run_id);
208
228
  });
209
229
  }
230
+ /**
231
+ * Write a status to a run row that is still `running`, and report whether the row changed. The first terminal write
232
+ * of a run wins: a row that already holds a terminal status keeps its status, its `completed_at`, and its error.
233
+ * The error text loses each NUL character, because Postgres rejects a NUL in text and the write must not fail for its own value.
234
+ */
210
235
  export function updateRunStatus(pool, runId, status, error) {
211
236
  const now = new Date().toISOString();
212
237
  const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled" || status === "suspended_insufficient_funds";
213
238
  return tryMutation("runs.updateRunStatus", async () => {
214
- await pool.query({
239
+ const result = await pool.query({
215
240
  text: `UPDATE cortex_runs
216
241
  SET status = $1, completed_at = $2, error = $3
217
- WHERE run_id = $4`,
218
- values: [status, isTerminal ? now : null, error ?? null, runId],
242
+ WHERE run_id = $4 AND status = 'running'`,
243
+ values: [status, isTerminal ? now : null, error === undefined || error === null ? null : stripNulCharacters(error), runId],
219
244
  });
245
+ return (result.rowCount ?? 0) > 0;
220
246
  });
221
247
  }
222
248
  /**
@@ -224,9 +250,8 @@ export function updateRunStatus(pool, runId, status, error) {
224
250
  * and recording `reason` in `error` (the same column `collectAndComplete`
225
251
  * stamps `external_cancel` into). Reports whether a row transitioned — false
226
252
  * means the run was already terminal (e.g. it completed concurrently with an
227
- * external cancel), and the caller MUST NOT fall back to `updateRunStatus`,
228
- * which would clobber that terminal status. The guarded set is the active set
229
- * `queryActiveRun` deduplicates on.
253
+ * external cancel). The guarded set is the active set `queryActiveRun`
254
+ * deduplicates on.
230
255
  */
231
256
  export function markRunCanceledIfActive(pool, runId, reason) {
232
257
  const now = new Date().toISOString();
@@ -264,20 +289,45 @@ export function queryRun(pool, runId) {
264
289
  return row ? mapRunRow(row) : null;
265
290
  });
266
291
  }
267
- export function queryRunsByAnalysis(pool, analysisId, { limit = 50, offset = 0 } = {}) {
292
+ /**
293
+ * One window of the runs of an analysis, newest start first. With `q`, the window holds only the runs that the text
294
+ * filter selects: the filter applies before `limit` and `offset`, and it does not change the order.
295
+ */
296
+ export function queryRunsByAnalysis(pool, analysisId, { limit = 50, offset = 0, q } = {}) {
268
297
  return tryQuery("runs.queryRunsByAnalysis", async () => {
298
+ const values = [analysisId, limit, offset];
299
+ const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
269
300
  const result = await pool.query({
270
301
  text: `SELECT run_id, analysis_id, thread_id, workflow_name,
271
302
  status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
272
303
  mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
273
304
  FROM cortex_runs
274
305
  WHERE analysis_id = $1
306
+ ${text}
275
307
  ORDER BY started_at DESC LIMIT $2 OFFSET $3`,
276
- values: [analysisId, limit, offset],
308
+ values,
277
309
  });
278
310
  return result.rows.map(mapRunRow);
279
311
  });
280
312
  }
313
+ /**
314
+ * The count of the runs of an analysis, in one statement. With `q`, the count holds only the runs that the text filter
315
+ * selects, thus it agrees with the windows of {@link queryRunsByAnalysis} for the same `q`.
316
+ */
317
+ export function countRunsByAnalysis(pool, analysisId, { q } = {}) {
318
+ return tryQuery("runs.countRunsByAnalysis", async () => {
319
+ const values = [analysisId];
320
+ const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
321
+ const result = await pool.query({
322
+ text: `SELECT COUNT(*)::int AS total
323
+ FROM cortex_runs
324
+ WHERE analysis_id = $1
325
+ ${text}`,
326
+ values,
327
+ });
328
+ return Number(result.rows[0]?.total ?? 0);
329
+ });
330
+ }
281
331
  /**
282
332
  * The run ids of one plan of one analysis, newest start first, at most `limit`.
283
333
  * The run id breaks a tie of start times, highest first. A plan with no run gives
@@ -295,6 +345,23 @@ export function queryRunIdsByPlan(pool, opts) {
295
345
  return result.rows.map((row) => row.run_id);
296
346
  });
297
347
  }
348
+ /**
349
+ * The newest run of one plan of one analysis, or `null` for a plan with no run. The order is the one of
350
+ * {@link queryRunIdsByPlan}: the latest start first, and the highest run id on a tie of start times.
351
+ */
352
+ export function queryNewestPlanRun(pool, opts) {
353
+ return tryQuery("runs.queryNewestPlanRun", async () => {
354
+ const result = await pool.query({
355
+ text: `SELECT run_id, started_at FROM cortex_runs
356
+ WHERE analysis_id = $1 AND plan_id = $2
357
+ ORDER BY started_at DESC, run_id DESC
358
+ LIMIT 1`,
359
+ values: [opts.analysisId, opts.planId],
360
+ });
361
+ const row = result.rows[0];
362
+ return row ? { runId: row.run_id, startedAt: row.started_at } : null;
363
+ });
364
+ }
298
365
  /**
299
366
  * Read the analysis's non-terminal run activity without coupling it to a
300
367
  * conversation thread. The total is counted before the detail limit so a
@@ -359,16 +426,20 @@ export function queryRunsForInspection(pool, analysisId, { limit = 50, offset =
359
426
  };
360
427
  });
361
428
  }
362
- export function queryRunsByThread(pool, analysisId, threadId, { limit = 50, offset = 0 } = {}) {
429
+ /** The runs of one thread of an analysis, newest start first. With `q`, the text filter applies before the window. */
430
+ export function queryRunsByThread(pool, analysisId, threadId, { limit = 50, offset = 0, q } = {}) {
363
431
  return tryQuery("runs.queryRunsByThread", async () => {
432
+ const values = [analysisId, threadId, limit, offset];
433
+ const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
364
434
  const result = await pool.query({
365
435
  text: `SELECT run_id, analysis_id, thread_id, workflow_name,
366
436
  status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
367
437
  mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
368
438
  FROM cortex_runs
369
439
  WHERE analysis_id = $1 AND thread_id = $2
440
+ ${text}
370
441
  ORDER BY started_at DESC LIMIT $3 OFFSET $4`,
371
- values: [analysisId, threadId, limit, offset],
442
+ values,
372
443
  });
373
444
  return result.rows.map(mapRunRow);
374
445
  });
@@ -74,4 +74,13 @@ export declare function updateStepExecution(pool: Querier, runId: string, stepId
74
74
  * these steps never started.
75
75
  */
76
76
  export declare function sweepPendingStepExecutions(pool: Querier, runId: string): ResultAsync<void, DbError>;
77
+ /**
78
+ * Settle a run's still-`running` rows to `canceled` after an engine cancel.
79
+ * A canceled child re-raises before it writes a terminal row, so this is the
80
+ * only writer for a step that the cancel cut. `canceled`, not `skipped`: these
81
+ * steps started. The `status = 'running'` guard keeps a terminal row that a
82
+ * child wrote first. `started_at` and `completed_at` are ISO 8601 `TEXT`, so
83
+ * the duration casts both; it is NULL when `started_at` is NULL.
84
+ */
85
+ export declare function cancelRunningStepExecutions(pool: Querier, runId: string, reason: string): ResultAsync<void, DbError>;
77
86
  export declare function queryStepsByRun(pool: Querier, runId: string): ResultAsync<StepExecutionRow[], DbError>;
@@ -136,6 +136,28 @@ export function sweepPendingStepExecutions(pool, runId) {
136
136
  });
137
137
  });
138
138
  }
139
+ /**
140
+ * Settle a run's still-`running` rows to `canceled` after an engine cancel.
141
+ * A canceled child re-raises before it writes a terminal row, so this is the
142
+ * only writer for a step that the cancel cut. `canceled`, not `skipped`: these
143
+ * steps started. The `status = 'running'` guard keeps a terminal row that a
144
+ * child wrote first. `started_at` and `completed_at` are ISO 8601 `TEXT`, so
145
+ * the duration casts both; it is NULL when `started_at` is NULL.
146
+ */
147
+ export function cancelRunningStepExecutions(pool, runId, reason) {
148
+ const now = new Date().toISOString();
149
+ return tryMutation("stepExecutions.cancelRunningStepExecutions", async () => {
150
+ await pool.query({
151
+ text: `UPDATE cortex_step_executions
152
+ SET status = 'canceled',
153
+ completed_at = $2::text,
154
+ duration_ms = (EXTRACT(EPOCH FROM ($2::text::timestamptz - started_at::timestamptz)) * 1000)::bigint,
155
+ error = $3
156
+ WHERE run_id = $1 AND status = 'running'`,
157
+ values: [runId, now, reason],
158
+ });
159
+ });
160
+ }
139
161
  export function queryStepsByRun(pool, runId) {
140
162
  return tryQuery("stepExecutions.queryStepsByRun", async () => {
141
163
  const result = await pool.query({
@@ -24,6 +24,8 @@ export interface AdHocRoute {
24
24
  readonly agentId: string;
25
25
  readonly resources: ResourceSpec;
26
26
  readonly rationale: string;
27
+ /** The capability phrase of the chosen agent, when the router copied one that the agent advertises. */
28
+ readonly capability?: string;
27
29
  /** The validated package entries of the step. Empty is a normal answer. */
28
30
  readonly packages: readonly string[];
29
31
  /** Every entry that the validation removed, with the reason for the caveat. */
@@ -1,6 +1,7 @@
1
1
  import { jsonSchema, tool as aiTool } from "ai";
2
2
  import { z } from "zod";
3
3
  import { PLANNABLE_AGENT_CATALOG } from "../agents/sandbox-catalog.js";
4
+ import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
4
5
  import { forSubAgent } from "../auth/types.js";
5
6
  import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../app/data-profile-orientation.js";
6
7
  import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
@@ -24,6 +25,7 @@ const resourcesSchema = z.object({
24
25
  });
25
26
  const routeSchema = z.object({
26
27
  agentId: z.string().optional().describe("The id of one specialist from the eligible catalog, copied exactly."),
28
+ capability: z.string().optional().describe("The one capability phrase of that specialist which this request uses, copied exactly from its capabilities."),
27
29
  resources: resourcesSchema
28
30
  .optional()
29
31
  .describe("The sandbox resources of the step, inside the stated ranges. Omit to take the defaults; a value outside the ranges is replaced by the defaults."),
@@ -241,6 +243,20 @@ export async function routeAdHocRequest(deps, input) {
241
243
  // never becomes a `fallbackClass`, because that class describes the route
242
244
  // as a whole and a step agent reads it as "the routing itself fell back".
243
245
  const { packages, droppedPackages } = await routePackages(candidate.packages, deps.resolvePackages, logger);
244
- logger.info("ad hoc route selected", { agentId, resources, rationale, failure, packages, droppedPackages });
245
- return { agentId, resources, rationale, packages, droppedPackages, ...(failure ? { fallbackClass: failure } : {}) };
246
+ // Kept only when the chosen agent really advertises it. A phrase that no agent advertises
247
+ // resolves no data requirement anyway, and carrying it would put a fabricated capability into
248
+ // the step and into the run record. An absent phrase is a normal answer, not a fallback class:
249
+ // most requests use a capability that declares nothing.
250
+ const requestedCapability = typeof candidate.capability === "string" ? candidate.capability.trim() : "";
251
+ const capability = (SANDBOX_AGENT_META[agentId]?.capabilities ?? []).includes(requestedCapability) ? requestedCapability : undefined;
252
+ logger.info("ad hoc route selected", { agentId, resources, rationale, failure, packages, droppedPackages, ...(capability ? { capability } : {}) });
253
+ return {
254
+ agentId,
255
+ resources,
256
+ rationale,
257
+ packages,
258
+ droppedPackages,
259
+ ...(capability ? { capability } : {}),
260
+ ...(failure ? { fallbackClass: failure } : {}),
261
+ };
246
262
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * inspectCapabilityData — the reference data a specialist capability cannot run without, and
3
+ * whether this store holds it.
4
+ *
5
+ * The method of a capability lives in a skill pack, and only a sandbox step agent reads a skill.
6
+ * Thus a conversation or planning surface knows a capability by its phrase alone, and it cannot
7
+ * name the datasets the method needs. It then does one of two wrong things: it calls the work
8
+ * impossible, or it designs a method around what it can see and substitutes for what it cannot.
9
+ *
10
+ * The declaration on the agent metadata is the one place a surface above the step can read, and
11
+ * this tool is how it reads it. The answer names an absent dataset by its CATALOG ID, because the
12
+ * id is what an install takes — a surface that can provision needs the identifier, not a
13
+ * description. Where the ids reach an agent matters: a prompt names no dataset, so the knowledge
14
+ * arrives through a tool, which carries its own description into the context at attach time.
15
+ */
16
+ import type { EnvironmentStorePaths } from "../config/environment-stores.js";
17
+ export type InspectCapabilityDataDeps = Pick<EnvironmentStorePaths, "refStorePath">;
18
+ /** One capability, its declared datasets, and what the store holds of them. */
19
+ export interface CapabilityDataRow {
20
+ readonly agentId: string;
21
+ readonly capability: string;
22
+ readonly installed: readonly string[];
23
+ readonly absent: readonly string[];
24
+ }
25
+ export interface CapabilityDataResult {
26
+ /** False when the store could not be answered for: every `absent` list is then unknown, not empty. */
27
+ readonly storeReadable: boolean;
28
+ readonly rows: readonly CapabilityDataRow[];
29
+ readonly content: string;
30
+ }
31
+ /**
32
+ * Build the tool. The store read happens per call, not at construction: a dataset installed during
33
+ * the conversation must show as installed on the next call, and a cached answer would have the
34
+ * agent offer a download the user already approved.
35
+ */
36
+ export declare function createInspectCapabilityDataTool(deps?: InspectCapabilityDataDeps): import("./define-tool.js").Tool<Record<string, never>, CapabilityDataResult>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * inspectCapabilityData — the reference data a specialist capability cannot run without, and
3
+ * whether this store holds it.
4
+ *
5
+ * The method of a capability lives in a skill pack, and only a sandbox step agent reads a skill.
6
+ * Thus a conversation or planning surface knows a capability by its phrase alone, and it cannot
7
+ * name the datasets the method needs. It then does one of two wrong things: it calls the work
8
+ * impossible, or it designs a method around what it can see and substitutes for what it cannot.
9
+ *
10
+ * The declaration on the agent metadata is the one place a surface above the step can read, and
11
+ * this tool is how it reads it. The answer names an absent dataset by its CATALOG ID, because the
12
+ * id is what an install takes — a surface that can provision needs the identifier, not a
13
+ * description. Where the ids reach an agent matters: a prompt names no dataset, so the knowledge
14
+ * arrives through a tool, which carries its own description into the context at attach time.
15
+ */
16
+ import { ok } from "neverthrow";
17
+ import { z } from "zod";
18
+ import { declaredDataRequirements } from "../agents/sandbox-catalog.js";
19
+ import { defineTool } from "./define-tool.js";
20
+ import { readInstalledDatasetIds } from "./sandbox/list-available-refs.js";
21
+ const NO_DECLARATIONS = "No capability of this install declares reference data that it cannot run without.";
22
+ const STORE_UNREADABLE = "The reference store could not be read, so whether these datasets are installed is UNKNOWN — not absent. " +
23
+ "Say that, and do not offer to install a dataset on this answer alone.";
24
+ function renderRows(rows, storeReadable) {
25
+ if (rows.length === 0)
26
+ return NO_DECLARATIONS;
27
+ const lines = rows.map((row) => {
28
+ const state = storeReadable
29
+ ? [row.absent.length > 0 ? `absent: ${row.absent.join(", ")}` : null, row.installed.length > 0 ? `installed: ${row.installed.join(", ")}` : null]
30
+ .filter((part) => part !== null)
31
+ .join(" | ")
32
+ : `declared: ${[...row.installed, ...row.absent].sort().join(", ")}`;
33
+ return `- ${row.agentId} — "${row.capability}"\n ${state}`;
34
+ });
35
+ const header = storeReadable
36
+ ? "Reference data a capability cannot run without, and what this store holds. An absent id is what an install takes."
37
+ : `Reference data a capability cannot run without. ${STORE_UNREADABLE}`;
38
+ return [header, "", ...lines].join("\n");
39
+ }
40
+ /**
41
+ * Build the tool. The store read happens per call, not at construction: a dataset installed during
42
+ * the conversation must show as installed on the next call, and a cached answer would have the
43
+ * agent offer a download the user already approved.
44
+ */
45
+ export function createInspectCapabilityDataTool(deps = {}) {
46
+ return defineTool({
47
+ id: "inspect_capability_data",
48
+ description: "The reference data a specialist capability cannot run without, and whether this install holds it. " +
49
+ "Call it BEFORE you design a method yourself, and before you tell the user that something cannot be done — a capability whose data is absent is one install away, not impossible, and this is the only place that names WHICH datasets. " +
50
+ "Each entry names a catalog dataset id, which is exactly what an install takes; a surface with a way to provision can act on it directly. " +
51
+ "It takes no arguments and the whole set is small, so one call is the complete answer. " +
52
+ "An unreadable store answers UNKNOWN rather than absent, so a store that cannot be read never reads as a store that holds nothing.",
53
+ inputSchema: z.object({}),
54
+ describeCall: () => "declared capability data",
55
+ execute: async () => {
56
+ const declared = declaredDataRequirements();
57
+ if (declared.length === 0)
58
+ return ok({ storeReadable: true, rows: [], content: NO_DECLARATIONS });
59
+ const installedIds = await readInstalledDatasetIds(deps);
60
+ const storeReadable = installedIds !== undefined;
61
+ const rows = declared.map((requirement) => {
62
+ const datasets = [...requirement.datasets].sort();
63
+ return {
64
+ agentId: requirement.agentId,
65
+ capability: requirement.capability,
66
+ installed: installedIds === undefined ? [] : datasets.filter((dataset) => installedIds.has(dataset)),
67
+ absent: installedIds === undefined ? datasets : datasets.filter((dataset) => !installedIds.has(dataset)),
68
+ };
69
+ });
70
+ return ok({ storeReadable, rows, content: renderRows(rows, storeReadable) });
71
+ },
72
+ });
73
+ }
@@ -17,7 +17,7 @@ import { type AnalysisPlan } from "../schemas/workflow-state.js";
17
17
  import type { ExecuteAnalysisInput, ExecuteAnalysisResult } from "../workflows/execute-analysis.js";
18
18
  import { type AdHocRoute } from "./ad-hoc-router.js";
19
19
  type ExecuteAnalysisWorkflow = (input: ExecuteAnalysisInput) => Promise<ExecuteAnalysisResult>;
20
- export interface ExecuteAnalysisToolDeps extends Pick<EnvironmentStorePaths, "farmLockFile" | "imagePackagesFile" | "readPoolInventory"> {
20
+ export interface ExecuteAnalysisToolDeps extends Pick<EnvironmentStorePaths, "farmLockFile" | "imagePackagesFile" | "readPoolInventory" | "refStorePath"> {
21
21
  readonly pool: Pool;
22
22
  readonly executeAnalysisWorkflow: ExecuteAnalysisWorkflow;
23
23
  readonly runAuthorizer: RunAuthorizer;
@@ -56,9 +56,22 @@ export declare class PlanPackagesMissingError extends Error {
56
56
  readonly unavailable?: string | undefined;
57
57
  constructor(missing: string[], collisions: string[], unavailable?: string | undefined);
58
58
  }
59
+ /**
60
+ * A declared dataset that the reference store does not hold, found at the pre-launch pass. The
61
+ * message names the datasets and no remedy command — the remedy belongs to the embedder, and its
62
+ * wrapper appends it.
63
+ */
64
+ export declare class PlanReferencesMissingError extends Error {
65
+ readonly missing: string[];
66
+ readonly unavailable?: string | undefined;
67
+ constructor(missing: string[], unavailable?: string | undefined);
68
+ }
59
69
  /** The deps that start the run of a stored plan. */
60
- export type LaunchPlanRunDeps = Pick<ExecuteAnalysisToolDeps, "pool" | "executeAnalysisWorkflow" | "runAuthorizer" | "runLauncher" | "resourcePolicy" | "extendAnalysisFarm" | "logger">;
61
- /** The run of a stored plan. `started` is false when a run of the plan was already active. */
70
+ export type LaunchPlanRunDeps = Pick<ExecuteAnalysisToolDeps, "pool" | "executeAnalysisWorkflow" | "runAuthorizer" | "runLauncher" | "resourcePolicy" | "extendAnalysisFarm" | "refStorePath" | "logger">;
71
+ /**
72
+ * The run of a stored plan. `started` is false when this call started nothing: `runId` then names the
73
+ * active run of the plan, or the newest run of the plan when the view of the caller was stale.
74
+ */
62
75
  export type PlanLaunch = {
63
76
  runId: string;
64
77
  started: boolean;
@@ -80,6 +93,13 @@ export type LaunchPlanRunRefusal = {
80
93
  missing: string[];
81
94
  collisions: string[];
82
95
  unavailable?: string;
96
+ } | {
97
+ type: "references_missing";
98
+ missing: string[];
99
+ unavailable?: string;
100
+ } | {
101
+ type: "run_not_found";
102
+ runId: string;
83
103
  } | {
84
104
  type: "not_authorized";
85
105
  reason: string;
@@ -94,18 +114,26 @@ export type LaunchPlanRunRefusal = {
94
114
  cause: unknown;
95
115
  };
96
116
  export declare function buildAdHocPlan(request: string, route: AdHocRoute, createdAt?: string): AnalysisPlan;
117
+ /**
118
+ * `newestRunId` is the newest run of the plan that the caller saw, or `null` when the caller saw no run.
119
+ * Without it, the launch compares no run.
120
+ */
121
+ export type LaunchPlanRunInput = {
122
+ session: AgentSession;
123
+ planId: string;
124
+ newestRunId?: string | null;
125
+ };
97
126
  /**
98
127
  * Start the run of a stored plan. It reads the plan under the analysis of the session, validates it,
99
- * links its packages, reserves a run, authorizes it, and starts the workflow. It gives the run id, or
100
- * the refusal, and it emits no chat part. `started` is false when a run of the plan was already active.
128
+ * compares the newest run of the plan with `newestRunId`, links its packages, reserves a run,
129
+ * authorizes it, and starts the workflow. It gives the run id, or the refusal, and it emits no chat
130
+ * part. `started` is false when this call started nothing: a run of the plan was already active, or the
131
+ * newest run of the plan is not the run that the caller saw. `runId` then names that run.
101
132
  *
102
133
  * This is an API boundary: a host calls it outside a tool body, thus no outcome above rejects the
103
134
  * promise.
104
135
  */
105
- export declare function launchPlanRun(deps: LaunchPlanRunDeps, input: {
106
- session: AgentSession;
107
- planId: string;
108
- }): ResultAsync<PlanLaunch, LaunchPlanRunRefusal>;
136
+ export declare function launchPlanRun(deps: LaunchPlanRunDeps, input: LaunchPlanRunInput): ResultAsync<PlanLaunch, LaunchPlanRunRefusal>;
109
137
  export declare function createExecuteAnalysisTool(deps: ExecuteAnalysisToolDeps): import("./define-tool.js").Tool<{
110
138
  mode: "plan" | "adhoc";
111
139
  planId?: string | undefined;