@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.
- package/dist/agents/conversation-agent.d.ts +10 -0
- package/dist/agents/conversation-agent.js +19 -1
- package/dist/agents/sandbox/shared.d.ts +23 -2
- package/dist/agents/sandbox/shared.js +51 -18
- package/dist/agents/sandbox/translational-safety-agent.js +26 -0
- package/dist/agents/sandbox/types.d.ts +24 -1
- package/dist/agents/sandbox-catalog.d.ts +14 -0
- package/dist/agents/sandbox-catalog.js +19 -0
- package/dist/app/approve-plan.d.ts +5 -2
- package/dist/app/approve-plan.js +1 -1
- package/dist/app/chat-turn.js +6 -3
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +12 -2
- package/dist/contracts/chat-events.js +1 -1
- package/dist/contracts/chat-frame.js +6 -2
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +25 -0
- package/dist/contracts/schemas/chat-events.js +7 -0
- package/dist/contracts/schemas/chat-parts.d.ts +39 -0
- package/dist/contracts/schemas/chat-parts.js +17 -0
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/dbos-workflow-purger.d.ts +2 -0
- package/dist/execution/dbos-workflow-purger.js +21 -14
- package/dist/execution/run-canceler.d.ts +33 -11
- package/dist/execution/run-canceler.js +129 -27
- package/dist/execution/run-end-record.d.ts +30 -0
- package/dist/execution/run-end-record.js +27 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +11 -1
- package/dist/loop/call-guard.d.ts +50 -0
- package/dist/loop/call-guard.js +90 -0
- package/dist/loop/compaction.d.ts +5 -0
- package/dist/loop/compaction.js +17 -0
- package/dist/loop/run-agent.d.ts +9 -0
- package/dist/loop/run-agent.js +15 -1
- package/dist/loop/run-to-terminal.js +4 -1
- package/dist/loop/types.d.ts +6 -0
- package/dist/memory/conversation-display-recorder.d.ts +0 -2
- package/dist/memory/conversation-display-recorder.js +28 -3
- package/dist/memory/thread-history.d.ts +34 -7
- package/dist/memory/thread-history.js +151 -42
- package/dist/prompts/briefing.d.ts +57 -3
- package/dist/prompts/briefing.js +129 -2
- package/dist/prompts/planner.js +13 -2
- package/dist/provenance/seam.d.ts +23 -15
- package/dist/provenance/seam.js +7 -5
- package/dist/providers/ai-sdk.js +87 -7
- package/dist/providers/types.d.ts +6 -0
- package/dist/reference-data/catalog.js +141 -2
- package/dist/report-model/authoring-grammar.d.ts +26 -26
- package/dist/sandbox/image-packages.d.ts +26 -4
- package/dist/sandbox/image-packages.js +18 -4
- package/dist/schemas/data-profile-schemas.d.ts +12 -12
- package/dist/schemas/plan-schemas.d.ts +57 -3
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/workflow-state.d.ts +113 -0
- package/dist/schemas/workflow-state.js +54 -0
- package/dist/state/artifacts.d.ts +17 -1
- package/dist/state/artifacts.js +21 -3
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +1 -1
- package/dist/state/runs.d.ts +43 -9
- package/dist/state/runs.js +85 -14
- package/dist/state/step-executions.d.ts +9 -0
- package/dist/state/step-executions.js +22 -0
- package/dist/tools/ad-hoc-router.d.ts +2 -0
- package/dist/tools/ad-hoc-router.js +18 -2
- package/dist/tools/capability-data.d.ts +36 -0
- package/dist/tools/capability-data.js +73 -0
- package/dist/tools/execute-analysis.d.ts +37 -9
- package/dist/tools/execute-analysis.js +131 -21
- package/dist/tools/knowledge/check.d.ts +60 -0
- package/dist/tools/knowledge/check.js +92 -0
- package/dist/tools/knowledge/client.d.ts +382 -0
- package/dist/tools/knowledge/client.js +302 -0
- package/dist/tools/knowledge/environment.d.ts +78 -0
- package/dist/tools/knowledge/environment.js +105 -0
- package/dist/tools/knowledge/index.d.ts +49 -0
- package/dist/tools/knowledge/index.js +70 -0
- package/dist/tools/knowledge/local-slots.d.ts +62 -0
- package/dist/tools/knowledge/local-slots.js +177 -0
- package/dist/tools/knowledge/pin.d.ts +14 -0
- package/dist/tools/knowledge/pin.js +17 -0
- package/dist/tools/knowledge/recommend.d.ts +92 -0
- package/dist/tools/knowledge/recommend.js +98 -0
- package/dist/tools/knowledge/situation.d.ts +143 -0
- package/dist/tools/knowledge/situation.js +164 -0
- package/dist/tools/knowledge/skeleton.d.ts +69 -0
- package/dist/tools/knowledge/skeleton.js +202 -0
- package/dist/tools/knowledge/template.d.ts +137 -0
- package/dist/tools/knowledge/template.js +267 -0
- package/dist/tools/research/generate-plan.d.ts +23 -0
- package/dist/tools/research/generate-plan.js +203 -12
- package/dist/tools/research/guarded-address.d.ts +20 -0
- package/dist/tools/research/guarded-address.js +148 -0
- package/dist/tools/research/searxng-engine.d.ts +40 -0
- package/dist/tools/research/searxng-engine.js +120 -0
- package/dist/tools/research/web-fetch.d.ts +124 -0
- package/dist/tools/research/web-fetch.js +627 -0
- package/dist/tools/research/web-search.d.ts +75 -0
- package/dist/tools/research/web-search.js +112 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
- package/dist/tools/sandbox/list-available-packages.js +12 -4
- package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
- package/dist/tools/sandbox/list-available-refs.js +60 -3
- package/dist/tools/sandbox/report-blocker.js +5 -1
- package/dist/tools/terminal-text.d.ts +11 -0
- package/dist/tools/terminal-text.js +19 -0
- package/dist/tools/workspace/decision-record.d.ts +81 -0
- package/dist/tools/workspace/decision-record.js +117 -0
- package/dist/tools/workspace/edit-file.d.ts +3 -1
- package/dist/tools/workspace/edit-file.js +20 -2
- package/dist/tools/workspace/execute-command.d.ts +17 -0
- package/dist/tools/workspace/execute-command.js +28 -1
- package/dist/tools/workspace/index.d.ts +1 -0
- package/dist/tools/workspace/index.js +1 -0
- package/dist/tools/workspace/mutator.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +58 -18
- package/dist/workflows/execute-analysis.js +273 -58
- package/dist/workflows/sandbox-step.d.ts +12 -0
- package/package.json +5 -1
package/dist/state/runs.js
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
|
228
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
100
|
-
*
|
|
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;
|