@vecteur/cli 0.5.5 → 0.5.8
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/bin/vecteur.js +34 -4
- package/package.json +1 -1
- package/src/client.js +50 -0
- package/src/contract.js +4 -3
- package/src/download.js +12 -2
- package/src/format.js +165 -10
- package/src/mcp.js +7 -5
package/bin/vecteur.js
CHANGED
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
clearCredentials, credentialsPath, readCredentials, setCurrentOrigin, writeCredentials, selectProject,
|
|
11
11
|
} from "../src/credentials.js";
|
|
12
|
-
import { downloadArtifact } from "../src/download.js";
|
|
12
|
+
import { downloadArtifact, downloadProjection } from "../src/download.js";
|
|
13
13
|
import { humanProjects, resolveNamed, resolveRunProject, resolveWorkspace } from "../src/projects.js";
|
|
14
|
-
import { canonicalJson, human, humanCost, progress } from "../src/format.js";
|
|
14
|
+
import { canonicalJson, currentTransferTables, human, humanCost, progress } from "../src/format.js";
|
|
15
15
|
|
|
16
16
|
function manifest() {
|
|
17
17
|
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -43,7 +43,7 @@ function refuseOldNode() {
|
|
|
43
43
|
|
|
44
44
|
function helpText(version) {
|
|
45
45
|
return `vecteur ${version}
|
|
46
|
-
login | logout | whoami | project list | project create <name> [--workspace <name-or-id>] | project use <name-or-id> | project get <id> | run submit [-p <id>] -a <ask> | run get <id> | run events <id> | artifact get <id> [--output <path>] [-p <id>] | mcp
|
|
46
|
+
login | logout | whoami | project list | project create <name> [--workspace <name-or-id>] | project use <name-or-id> | project get <id> | run submit [-p <id>] -a <ask> | run get <id> | run events <id> | artifact get <id> [--output <path>] [-p <id>] | projection get <run-id> <projection-id> [--output <path>] [-p <id>] | mcp
|
|
47
47
|
--help --version --json --origin <url> --no-browser --local -p/--project -a/--ask --output <path>
|
|
48
48
|
`;
|
|
49
49
|
}
|
|
@@ -156,6 +156,10 @@ function parseCommand(argv) {
|
|
|
156
156
|
&& ask === undefined) {
|
|
157
157
|
return { kind: "artifact_get", id: args[2], projectId, output, origin, json };
|
|
158
158
|
}
|
|
159
|
+
if (args.length === 4 && args[0] === "projection" && args[1] === "get"
|
|
160
|
+
&& value(args[2]) && value(args[3]) && ask === undefined) {
|
|
161
|
+
return { kind: "projection_get", runId: args[2], id: args[3], projectId, output, origin, json };
|
|
162
|
+
}
|
|
159
163
|
throw new CliError("usage_invalid", "Invalid command grammar");
|
|
160
164
|
}
|
|
161
165
|
|
|
@@ -319,11 +323,23 @@ async function main() {
|
|
|
319
323
|
: `Downloaded ${value.bytes} bytes to ${value.path} (${value.content_type})\n`);
|
|
320
324
|
return;
|
|
321
325
|
}
|
|
326
|
+
else if (command.kind === "projection_get") {
|
|
327
|
+
const projectId = await resolveRunProject(client, command.projectId);
|
|
328
|
+
const run = await client.runGet(command.runId);
|
|
329
|
+
const matches = run.events.filter(event => event.kind === "engineering_projection"
|
|
330
|
+
&& event.payload?.projection_id === command.id && event.payload?.kind === "transfer_table");
|
|
331
|
+
if (matches.length !== 1) throw new CliError("projection_invalid", "Run has no unique transfer table projection");
|
|
332
|
+
value = await downloadProjection(client, projectId, command.runId, matches[0].payload, command.output);
|
|
333
|
+
process.stdout.write(command.json ? canonicalJson(value)
|
|
334
|
+
: `Downloaded ${value.bytes} bytes to ${value.path} (${value.content_type}; ${value.disposition})\n`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
322
337
|
else if (command.kind === "run_submit") {
|
|
323
338
|
const projectId = await resolveRunProject(client, command.projectId);
|
|
324
339
|
value = await client.runSubmit(projectId, command.ask, onEvent);
|
|
325
340
|
if (!command.json) {
|
|
326
|
-
|
|
341
|
+
const currentTransferIds = await verifiedTransferIds(client, value);
|
|
342
|
+
process.stdout.write(human(value, { projectId, currentTransferIds }));
|
|
327
343
|
try {
|
|
328
344
|
const cost = await client.customerCost(projectId);
|
|
329
345
|
process.stdout.write(`\n${humanCost(cost)}`);
|
|
@@ -341,9 +357,23 @@ async function main() {
|
|
|
341
357
|
if (!command.json && !renderOptions.projectId && config.projectId) {
|
|
342
358
|
renderOptions = { projectId: config.projectId };
|
|
343
359
|
}
|
|
360
|
+
if (!command.json && Array.isArray(value?.events)) {
|
|
361
|
+
renderOptions.currentTransferIds = await verifiedTransferIds(client, value);
|
|
362
|
+
}
|
|
344
363
|
process.stdout.write(command.json ? canonicalJson(value) : human(value, renderOptions));
|
|
345
364
|
}
|
|
346
365
|
|
|
366
|
+
async function verifiedTransferIds(client, run) {
|
|
367
|
+
const ids = new Set();
|
|
368
|
+
for (const projection of currentTransferTables(run.events ?? [])) {
|
|
369
|
+
const read = await client.projectionGet(
|
|
370
|
+
projection.project_id, run.run_id, projection, async () => {},
|
|
371
|
+
);
|
|
372
|
+
if (read.disposition === "current") ids.add(projection.projection_id);
|
|
373
|
+
}
|
|
374
|
+
return ids;
|
|
375
|
+
}
|
|
376
|
+
|
|
347
377
|
main().catch((error) => {
|
|
348
378
|
const code = error instanceof CliError ? error.code : "internal_error";
|
|
349
379
|
process.stderr.write(`${code}: ${redact(error?.message ?? "CLI failed")}\n`);
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -535,6 +535,56 @@ export class VecteurClient {
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
async projectionGet(projectId, runId, projection, onChunk, signal = undefined) {
|
|
539
|
+
const ref = projection?.payload_ref;
|
|
540
|
+
if (typeof projectId !== "string" || !projectId || typeof runId !== "string" || !runId
|
|
541
|
+
|| projection?.kind !== "transfer_table" || projection?.run_id !== runId
|
|
542
|
+
|| projection?.project_id !== projectId
|
|
543
|
+
|| !/^projection-[0-9a-f]{64}$/.test(projection?.projection_id ?? "")
|
|
544
|
+
|| ref?.schema !== "vecteur.owner-transfer-table.v1"
|
|
545
|
+
|| ref?.media_type !== "application/vnd.vecteur.owner-transfer-table+json"
|
|
546
|
+
|| !/^[0-9a-f]{64}$/.test(ref?.payload_sha256 ?? "")
|
|
547
|
+
|| typeof onChunk !== "function") {
|
|
548
|
+
throw new CliError("projection_invalid", "projection get requires a sealed transfer table on this Run");
|
|
549
|
+
}
|
|
550
|
+
const route = `/api/projects/${encodeURIComponent(projectId)}/runs/${encodeURIComponent(runId)}`
|
|
551
|
+
+ `/projections/${encodeURIComponent(projection.projection_id)}/payload`;
|
|
552
|
+
const response = await this.request("GET", route, {
|
|
553
|
+
accept: ref.media_type, acceptEncoding: "identity", signal,
|
|
554
|
+
});
|
|
555
|
+
try {
|
|
556
|
+
if (response.headers.get("content-type")?.split(";", 1)[0] !== ref.media_type
|
|
557
|
+
|| (response.headers.get("content-encoding") ?? "identity").toLowerCase() !== "identity") {
|
|
558
|
+
throw new CliError("projection_invalid", "Server returned the wrong projection media type");
|
|
559
|
+
}
|
|
560
|
+
const disposition = response.headers.get("x-vecteur-projection-disposition");
|
|
561
|
+
if (disposition !== "current" && disposition !== "historical") {
|
|
562
|
+
throw new CliError("projection_invalid", "Server returned no valid projection disposition");
|
|
563
|
+
}
|
|
564
|
+
const declared = response.headers.get("content-length");
|
|
565
|
+
if (declared === null || !/^(?:0|[1-9][0-9]*)$/.test(declared)
|
|
566
|
+
|| Number(declared) > 256 * 1024 || !response.body) {
|
|
567
|
+
throw new CliError("projection_invalid", "Projection byte length is invalid");
|
|
568
|
+
}
|
|
569
|
+
const digest = createHash("sha256");
|
|
570
|
+
let bytes = 0;
|
|
571
|
+
for await (const chunk of response.body) {
|
|
572
|
+
response[RESPONSE_ACTIVITY]?.();
|
|
573
|
+
bytes += chunk.byteLength;
|
|
574
|
+
if (bytes > Number(declared)) throw new CliError("projection_invalid", "Projection exceeds its declared size");
|
|
575
|
+
digest.update(chunk);
|
|
576
|
+
await onChunk(chunk);
|
|
577
|
+
}
|
|
578
|
+
if (bytes !== Number(declared) || digest.digest("hex") !== ref.payload_sha256) {
|
|
579
|
+
throw new CliError("projection_invalid", "Projection bytes differ from RunWire");
|
|
580
|
+
}
|
|
581
|
+
return { projection_id: projection.projection_id, project_id: projectId,
|
|
582
|
+
run_id: runId, content_type: ref.media_type, bytes, disposition };
|
|
583
|
+
} finally {
|
|
584
|
+
response[RELEASE_TIMEOUT]?.();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
538
588
|
async runSubmit(projectId, ask, onEvent = null, signal = undefined) {
|
|
539
589
|
const value = await this.#json("POST", `/api/projects/${encodeURIComponent(projectId)}/runs`, {
|
|
540
590
|
body: { ask, attachments: [], parent_state_hash: null, ui_context: null },
|
package/src/contract.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-contract.mjs from owner TypeScript projections.
|
|
2
|
-
export const OWNER_CONTRACT_IDENTITY = "api-sha256:
|
|
2
|
+
export const OWNER_CONTRACT_IDENTITY = "api-sha256:8f81ea544eecd9e81da3fd56ad816ac24991cb0e68f3b1cb45180711eaf16056;run-wire-sha256:e474079a0453c07e8249eddfc6781bb0c053ee81d08b0c7988e0670ad14d302b";
|
|
3
3
|
export const RECOVERY_ACTIONS = Object.freeze({"delete_project":{"label":"Delete a project","href":"/projects"},"upgrade_plan":{"label":"Upgrade your plan","href":"/dashboard/billing"},"wait_for_reset":{"label":"Wait until the UTC reset","href":null}});
|
|
4
|
-
export const EVENT_KINDS = Object.freeze(["source","step_started","step_finished","tool_call","tool_result","synthesis","artifact","terminal","heartbeat","assistant_delta","knowledge_binding","engineering_projection","provider_attempt","clarification","input_rows"]);
|
|
4
|
+
export const EVENT_KINDS = Object.freeze(["source","step_started","step_finished","tool_call","tool_result","synthesis","artifact","terminal","heartbeat","assistant_delta","knowledge_binding","engineering_projection","provider_attempt","clarification","clarification_evidence","input_rows"]);
|
|
5
5
|
export const RETIRED_EVENT_KINDS = Object.freeze(["context_frame","cta","step_label"]);
|
|
6
6
|
export const TERMINAL_FIELDS = Object.freeze(["answer","defect","deliverable_asked","evidence_dag","instruction_source","intent","missing_input_need","missing_inputs","provider_retry_offer","recovery","refusal_unfounded","state","steps","unmet_wanted"]);
|
|
7
|
+
export const TERMINAL_STATES = Object.freeze(["ok","partial","blocked","refused","infeasible_physics","capability_gap"]);
|
|
7
8
|
export const ARTIFACT_MEDIA_TYPES = Object.freeze({"binary":"application/octet-stream","code":"text/plain","csv":"text/csv","docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","drive":"application/vnd.google-drive","html":"text/html","jpeg":"image/jpeg","json":"application/json","md":"text/markdown","pdf":"application/pdf","png":"image/png","svg":"image/svg+xml","xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});
|
|
8
9
|
export const TERMINAL_RECOVERY_DECLARATIONS = Object.freeze({"answer_now":[["start_run",["project_id"]]],"artifact_publication_missing":[["start_run",["project_id"]]],"artifact_store_unavailable":[["start_run",["project_id"]]],"budget_exhausted":[["start_run",["project_id"]]],"cancelled":[["start_run",["project_id"]]],"cause_unrecorded":[["start_run",["project_id"]]],"clarification_custody_invalid":[["start_run",["project_id"]]],"clarification_custody_lost":[["start_run",["project_id"]]],"clarification_resume_invalid":[["start_run",["project_id"]]],"clarification_unpersisted":[["start_run",["project_id"]]],"context_unselected":[["start_run",["project_id"]]],"credential_class_disclosure":[["start_run",["project_id"]]],"deliverable_missing":[["start_run",["project_id"]]],"design_state_missing":[["start_run",["project_id"]]],"evidence_admission_invalid":[["start_run",["project_id"]]],"evidence_artifact_invalid":[["start_run",["project_id"]]],"evidence_artifact_unbound":[["start_run",["project_id"]]],"evidence_binding_unresolved":[["start_run",["project_id"]]],"evidence_context_invalid":[["start_run",["project_id"]]],"evidence_extract_unreached":[["start_run",["project_id"]]],"evidence_missing":[["start_run",["project_id"]]],"evidence_owner_receipt_mismatch":[["start_run",["project_id"]]],"evidence_owner_receipt_missing":[["start_run",["project_id"]]],"evidence_payload_invalid":[["start_run",["project_id"]]],"evidence_reached_invalid":[["start_run",["project_id"]]],"evidence_replay_invalid":[["start_run",["project_id"]]],"evidence_run_mismatch":[["start_run",["project_id"]]],"evidence_seal_collision":[["start_run",["project_id"]]],"evidence_selected_unreached":[["start_run",["project_id"]]],"execution_route_invalid":[["start_run",["project_id"]]],"executor_root_invalid":[["start_run",["project_id"]]],"extraction_offer_invalid":[["start_run",["project_id"]]],"extraction_offer_tampered":[["start_run",["project_id"]]],"fact_support_unread":[["start_run",["project_id"]]],"harness_arm_invalid":[["start_run",["project_id"]]],"harness_event_unmapped":[["start_run",["project_id"]]],"harness_exited":[["start_run",["project_id"]]],"harness_mapping_failed":[["start_run",["project_id"]]],"harness_session_invalid":[["start_run",["project_id"]]],"harness_session_missing":[["start_run",["project_id"]]],"harness_timeout":[["start_run",["project_id"]]],"infeasible_physics":[["start_run",["project_id"]]],"lab_denied":[["start_run",["project_id"]]],"lab_failed":[["start_run",["project_id"]]],"lab_not_reproduced":[["start_run",["project_id"]]],"lease_interval_invalid":[["start_run",["project_id"]]],"ledger_malformed":[["start_run",["project_id"]]],"max_tokens":[["start_run",["project_id"]]],"no_facts":[["start_run",["project_id"]]],"not_a_decision_i_can_make":[["start_run",["project_id"]]],"out_of_physics":[["start_run",["project_id"]]],"profile_eligibility_changed":[["start_run",["project_id"]]],"prompt_provenance_unavailable":[["start_run",["project_id"]]],"provider_budget_exhausted":[["start_run",["project_id"]]],"provider_budget_missing":[["start_run",["project_id"]]],"provider_budget_unavailable":[["start_run",["project_id"]]],"provider_relay_absent":[["start_run",["project_id"]]],"provider_route_control_invalid":[["start_run",["project_id"]]],"provider_route_exhausted":[["start_run",["project_id"]],["retry_run",["source_run_id","offer_id","generation"]]],"provider_route_failure_unobserved":[["start_run",["project_id"]]],"provider_route_reconciliation_invalid":[["start_run",["project_id"]]],"provider_route_restart_terminal":[["start_run",["project_id"]]],"provider_training_opted_in":[["start_run",["project_id"]]],"publish_seal_missing":[["start_run",["project_id"]]],"refusal_unfounded":[["start_run",["project_id"]]],"replay_capture_digest_mismatch":[["start_run",["project_id"]]],"replay_capture_invalid":[["start_run",["project_id"]]],"replay_config_incomplete":[["start_run",["project_id"]]],"replay_failed":[["start_run",["project_id"]]],"replay_no_requests":[["start_run",["project_id"]]],"replay_patch_invalid":[["start_run",["project_id"]]],"replay_plugin_absent":[["start_run",["project_id"]]],"replay_provider_socket":[["start_run",["project_id"]]],"replay_receipt_invalid":[["start_run",["project_id"]]],"replay_receipt_owner_chown":[["start_run",["project_id"]]],"replay_receipt_owner_incomplete":[["start_run",["project_id"]]],"replay_receipt_owner_invalid":[["start_run",["project_id"]]],"replay_request_observation_count":[["start_run",["project_id"]]],"replay_request_observation_invalid":[["start_run",["project_id"]]],"replay_request_observation_missing":[["start_run",["project_id"]]],"replay_request_owner_chown":[["start_run",["project_id"]]],"replay_socket_observation_invalid":[["start_run",["project_id"]]],"replay_socket_observation_missing":[["start_run",["project_id"]]],"replay_socket_owner_chown":[["start_run",["project_id"]]],"row_budget":[["start_run",["project_id"]]],"run_database_capability_refused":[["start_run",["project_id"]]],"runtime_unavailable":[["start_run",["project_id"]]],"sealed_contract_violated":[["start_run",["project_id"]]],"sealed_file_missing":[["start_run",["project_id"]]],"seat_patch_absent":[["start_run",["project_id"]]],"seat_patch_drift":[["start_run",["project_id"]]],"selection_admission_invalid":[["start_run",["project_id"]]],"selection_context_invalid":[["start_run",["project_id"]]],"selection_receipt_conflict":[["start_run",["project_id"]]],"selection_receipt_invalid":[["start_run",["project_id"]]],"selection_receipt_missing":[["start_run",["project_id"]]],"selection_required":[["start_run",["project_id"]]],"state_corrupt":[["start_run",["project_id"]]],"state_file_invalid":[["start_run",["project_id"]]],"state_invalid":[["start_run",["project_id"]]],"state_payload_overflow":[["start_run",["project_id"]]],"state_unencodable":[["start_run",["project_id"]]],"state_version_unknown":[["start_run",["project_id"]]],"unreceipted_numbers":[["start_run",["project_id"]]],"usage_egress_mismatch":[["start_run",["project_id"]]],"wall_budget":[["start_run",["project_id"]]],"wanted_not_computed":[["start_run",["project_id"]]]});
|
|
9
10
|
export const ORIGIN_KINDS = Object.freeze(["user","hypothesis","derived","knowledge","file","catalog","default"]);
|
|
@@ -47,6 +48,6 @@ const dimension=(v)=>shape(v,["uncached_input","cache_read","cache_write","outpu
|
|
|
47
48
|
const usage=(v)=>exact(v,["coverage","provider_id","model_id","observed_at","billable_dimensions"])&&["complete","incomplete"].includes(v.coverage)&&[v.provider_id,v.model_id].every(text)&&integer(v.observed_at)&&v.observed_at>=0&&Array.isArray(v.billable_dimensions)&&v.billable_dimensions.length>0&&v.billable_dimensions.every(dimension);
|
|
48
49
|
const control=v=>ownerRunControl(v)&&/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(v.id)&&integer(v.sequence)&&v.sequence>=1&&v.sequence<=32&&(v.kind==="steer"?typeof v.text==="string"&&v.text.trim().length>0&&Buffer.byteLength(v.text,"utf8")<=8192:v.text===undefined||v.text===null)&&((v.attempt===null&&v.step===null)||(integer(v.attempt)&&v.attempt>=0&&integer(v.step)&&v.step>0))&&(v.egress===null||(integer(v.egress.candidate)&&v.egress.candidate>=0&&integer(v.egress.call)&&v.egress.call>=0))&&(v.state==="queued"?v.attempt===null&&v.egress===null:v.state==="claimed"?v.attempt!==null&&v.egress===null:v.state==="dispatched"?v.attempt!==null&&v.egress!==null:v.egress===null);
|
|
49
50
|
const controls=v=>Array.isArray(v)&&v.length<=32&&v.every(control)&&new Set(v.map(x=>x.id)).size===v.length&&v.every((x,i)=>i===0||v[i-1].sequence<x.sequence);
|
|
50
|
-
export const validateAdmitted=(v)=>shape(v,["run_id","state_hash","result_hash","requested_route_id","profile","terminal","usage","events","controls"],["duration_seconds"])&&[v.run_id,v.state_hash,v.
|
|
51
|
+
export const validateAdmitted=(v)=>shape(v,["run_id","state_hash","result_hash","requested_route_id","profile","terminal","usage","events","controls"],["duration_seconds"])&&[v.run_id,v.state_hash,v.requested_route_id,v.profile].every(text)&&((v.terminal===""&&v.result_hash===""&&Array.isArray(v.events)&&(v.events.length===0||v.events.at(-1)?.kind!=="terminal"))||(TERMINAL_STATES.includes(v.terminal)&&text(v.result_hash)))&&usage(v.usage)&&Array.isArray(v.events)&&controls(v.controls)&&(v.duration_seconds===undefined||v.duration_seconds===null||(integer(v.duration_seconds)&&v.duration_seconds>=0));
|
|
51
52
|
export const validatePatCreated=(v)=>exact(v,["id","name","token","prefix","project_id","scopes","created_at","expires_at"])&&[v.id,v.name,v.token,v.prefix].every(text)&&(v.project_id===null||text(v.project_id))&&Array.isArray(v.scopes)&&v.scopes.every(s=>["project:read","project:execute"].includes(s))&&integer(v.created_at)&&integer(v.expires_at);
|
|
52
53
|
export const validateDeviceAuthorization=(v)=>exact(v,["device_code","user_code","verification_uri","verification_uri_complete","expires_at","interval"])&&[v.device_code,v.user_code,v.verification_uri,v.verification_uri_complete].every(text)&&integer(v.expires_at)&&integer(v.interval)&&v.interval>=0;
|
package/src/download.js
CHANGED
|
@@ -14,12 +14,22 @@ function defaultName(casRef) {
|
|
|
14
14
|
* a digest-derived basename, so traversal and shell metacharacters cannot arrive from RunWire.
|
|
15
15
|
*/
|
|
16
16
|
export async function downloadArtifact(client, casRef, projectId, output) {
|
|
17
|
-
|
|
17
|
+
return downloadTo(output ?? defaultName(casRef),
|
|
18
|
+
chunk => client.artifactGet(casRef, projectId, chunk));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function downloadProjection(client, projectId, runId, projection, output) {
|
|
22
|
+
return downloadTo(output ?? `transfer-table-${projection.projection_id.slice(11, 23)}.json`,
|
|
23
|
+
chunk => client.projectionGet(projectId, runId, projection, chunk));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function downloadTo(filename, fetchBytes) {
|
|
27
|
+
const target = resolve(filename);
|
|
18
28
|
const temporary = resolve(dirname(target), `.vecteur-${basename(target)}-${randomUUID()}.part`);
|
|
19
29
|
let handle;
|
|
20
30
|
try {
|
|
21
31
|
handle = await open(temporary, "wx", 0o600);
|
|
22
|
-
const result = await
|
|
32
|
+
const result = await fetchBytes(chunk => handle.writeFile(chunk));
|
|
23
33
|
await handle.sync();
|
|
24
34
|
await handle.close();
|
|
25
35
|
handle = undefined;
|
package/src/format.js
CHANGED
|
@@ -90,17 +90,133 @@ const CAUSE_WORD = Object.freeze({
|
|
|
90
90
|
answer_now: "stopped to answer now; what it printed stands",
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* THE ENGINEERING DOOR, SPOKEN BY THIS DOOR TOO (v2.4 S3; the web reads the same bytes in
|
|
95
|
+
* `frontend/src/run/door.ts`, and both readers are held to `tests/fixtures/door-observations.json`).
|
|
96
|
+
*
|
|
97
|
+
* A `python` call whose summary is `run <verb>` is a call of one published engineering method;
|
|
98
|
+
* its result's stdout is the bounded observation `laplace_ope/door_tail.py` prints. The reader is
|
|
99
|
+
* tolerant: the excerpt keeps a long stream's first and last lines around `[…]`, so every section
|
|
100
|
+
* is optional and the last `RESULT:` line is the headline of last resort.
|
|
101
|
+
*/
|
|
102
|
+
const DOOR_SUMMARY = /^run ([a-z][a-z0-9]*(?:_[a-z0-9]+)+)$/;
|
|
103
|
+
const DOOR_HEAD = /^([a-z][a-z0-9_]*): ok\b/;
|
|
104
|
+
const QUANTITY_LINE = /^ {2}(\S+) = (.*?)(?: {2}-- (.*))?$/;
|
|
105
|
+
const SECTION_OPEN = /^(inputs \(as given\):|inputs the owner defaulted|not set \(|quantities:|validity:|warnings:|files:|RESULT:)/;
|
|
106
|
+
const NUMBER = /^(-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|true|false|nan|-?inf)(?: (.+))?$/;
|
|
107
|
+
|
|
108
|
+
function quantityParts(text) {
|
|
109
|
+
const trimmed = text.trim();
|
|
110
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) return { value: trimmed, unit: "" };
|
|
111
|
+
const number = NUMBER.exec(trimmed);
|
|
112
|
+
return number ? { value: number[1], unit: (number[2] ?? "").trim() } : { value: trimmed, unit: "" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `{verb, headline: [{id, label, value, unit}], warnings}` of an observation, or null. */
|
|
116
|
+
export function doorObservation(text) {
|
|
117
|
+
const lines = String(text ?? "").split("\n").map(line => line.replace(/\s+$/, ""));
|
|
118
|
+
const head = DOOR_HEAD.exec(lines.find(line => line.trim()) ?? "");
|
|
119
|
+
if (!head) return null;
|
|
120
|
+
const cut = new Set();
|
|
121
|
+
lines.forEach((line, index) => {
|
|
122
|
+
if (line.trim() === "[…]") [index - 1, index, index + 1].forEach(n => cut.add(n));
|
|
123
|
+
});
|
|
124
|
+
const quantities = [];
|
|
125
|
+
const warnings = [];
|
|
126
|
+
let resultLine = "";
|
|
127
|
+
let section = "";
|
|
128
|
+
for (const [index, line] of lines.entries()) {
|
|
129
|
+
if (index === 0 || cut.has(index)) continue;
|
|
130
|
+
const open = SECTION_OPEN.exec(line);
|
|
131
|
+
if (open) {
|
|
132
|
+
section = open[1];
|
|
133
|
+
if (section === "warnings:" && line.includes("none")) section = "";
|
|
134
|
+
if (section === "RESULT:") resultLine = line.slice("RESULT:".length).trim();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (section === "quantities:") {
|
|
138
|
+
const row = QUANTITY_LINE.exec(line);
|
|
139
|
+
if (row) quantities.push({ id: row[1], label: row[3]?.trim() || row[1].replaceAll("_", " "), ...quantityParts(row[2]) });
|
|
140
|
+
} else if (section === "warnings:" && /^ {2}- /.test(line)) {
|
|
141
|
+
warnings.push(line.replace(/^ {2}- /, "").trim());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const byId = new Map(quantities.map(quantity => [quantity.id, quantity]));
|
|
145
|
+
const headline = [];
|
|
146
|
+
for (const part of resultLine.split(/; (?=\S+ = )/)) {
|
|
147
|
+
const row = /^(\S+) = (.*)$/.exec(part.trim());
|
|
148
|
+
if (!row) continue;
|
|
149
|
+
headline.push({ id: row[1], label: byId.get(row[1])?.label ?? row[1].replaceAll("_", " "), ...quantityParts(row[2]) });
|
|
150
|
+
}
|
|
151
|
+
// No parsable RESULT line: the last three physical quantities stand in (else the first three).
|
|
152
|
+
if (!headline.length) headline.push(...quantities.filter(quantity => quantity.unit).slice(-3));
|
|
153
|
+
if (!headline.length) headline.push(...quantities.slice(0, 3));
|
|
154
|
+
return { verb: head[1], headline, warnings };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The door calls of each row, by `step_id`: `{purpose, verb, observation, refusal}` in call order. */
|
|
158
|
+
function doorCallsByStep(events) {
|
|
159
|
+
const results = new Map();
|
|
160
|
+
for (const event of events) {
|
|
161
|
+
if (event?.kind === "tool_result" && typeof event.payload?.call_id === "string") {
|
|
162
|
+
results.set(event.payload.call_id, event.payload);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const rows = new Map();
|
|
166
|
+
for (const event of events) {
|
|
167
|
+
const call = event?.kind === "tool_call" ? event.payload : null;
|
|
168
|
+
const verb = call?.tool === "python" ? DOOR_SUMMARY.exec(String(call.summary ?? "").trim())?.[1] : null;
|
|
169
|
+
if (!verb) continue;
|
|
170
|
+
const result = results.get(call.call_id);
|
|
171
|
+
const observation = result?.status === "ok" ? doorObservation(result.stdout_excerpt) : null;
|
|
172
|
+
if (result?.status === "ok" && observation?.verb !== verb) continue; // free code that opened with `# run x_y`
|
|
173
|
+
const refusal = result && result.status !== "ok"
|
|
174
|
+
? String(result.stderr_excerpt || result.stdout_excerpt || "").trim().split("\n").filter(Boolean).slice(-3).join(" · ") : "";
|
|
175
|
+
const list = rows.get(call.step_id) ?? [];
|
|
176
|
+
list.push({ purpose: String(call.title ?? "").trim(), verb, observation, refusal, running: !result });
|
|
177
|
+
rows.set(call.step_id, list);
|
|
178
|
+
}
|
|
179
|
+
return rows;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function doorLine({ purpose, verb, observation, refusal, running }) {
|
|
183
|
+
const said = observation
|
|
184
|
+
? observation.headline.map(quantity => `${quantity.label} ${quantity.value}${quantity.unit ? ` ${quantity.unit}` : ""}`).join(", ")
|
|
185
|
+
: (refusal ? `refused: ${refusal}` : running ? "running" : "computed");
|
|
186
|
+
const lines = [` ▸ ${plainLine(purpose || verb)} — ${verb}: ${plainLine(said)}`];
|
|
187
|
+
for (const warning of (observation?.warnings ?? []).slice(0, 2)) lines.push(` ! ${plainLine(warning)}`);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
93
191
|
export function taskLines(events) {
|
|
192
|
+
const doors = doorCallsByStep(events);
|
|
193
|
+
// The conclusion is engineering work (it prints the answer from evidence); its text is the
|
|
194
|
+
// synthesis, so the row stays one line.
|
|
195
|
+
const concluded = new Set(events.filter((event) => event?.kind === "tool_call" && event.payload?.tool === "conclude")
|
|
196
|
+
.map((event) => event.payload.step_id));
|
|
197
|
+
// Only a run that called the door tells engineering rows from execution rows; any other run
|
|
198
|
+
// prints exactly as before.
|
|
199
|
+
const distinguishes = doors.size > 0 || concluded.size > 0;
|
|
94
200
|
return events.flatMap((event) => {
|
|
95
201
|
if (event?.kind !== "step_finished") return [];
|
|
96
202
|
const row = event.payload;
|
|
97
203
|
if (!row || typeof row !== "object" || typeof row.step_id !== "string") return [];
|
|
204
|
+
const outcome = CAUSE_WORD[row.cause] ?? row.status;
|
|
205
|
+
const result = typeof row.result_line === "string" && row.result_line.trim() ? row.result_line : "";
|
|
206
|
+
if (distinguishes) {
|
|
207
|
+
const calls = doors.get(row.step_id);
|
|
208
|
+
if (calls?.length) {
|
|
209
|
+
const rest = row.status === "computed" ? [] : [` (${plainLine(row.title)} — ${outcome})`];
|
|
210
|
+
return [[...calls.map(doorLine), ...rest].join("\n")];
|
|
211
|
+
}
|
|
212
|
+
if (row.status === "asked" || concluded.has(row.step_id)) return [` ▸ ${plainLine(`${row.title} — ${outcome}`)}`];
|
|
213
|
+
// Execution, compressed to one line: what ran, how it ended, what it printed.
|
|
214
|
+
return [` · ${plainLine(`${row.title} — ${outcome}${result ? `: ${result}` : ""}`)}`];
|
|
215
|
+
}
|
|
98
216
|
const attempts = Number.isInteger(row.attempts) && row.attempts > 0
|
|
99
217
|
? ` · ${row.attempts} ${row.attempts === 1 ? "attempt" : "attempts"}` : "";
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const outcome = CAUSE_WORD[row.cause] ?? row.status;
|
|
103
|
-
return [` ${plainLine(`${row.title} — ${outcome}${attempts}`)}${result}`];
|
|
218
|
+
const printed = result ? `\n ${plainLine(result)}` : "";
|
|
219
|
+
return [` ${plainLine(`${row.title} — ${outcome}${attempts}`)}${printed}`];
|
|
104
220
|
});
|
|
105
221
|
}
|
|
106
222
|
|
|
@@ -207,8 +323,30 @@ function recoveryLine(action) {
|
|
|
207
323
|
return `Recovery: ${action.capability}${args ? ` ${args}` : ""}`;
|
|
208
324
|
}
|
|
209
325
|
|
|
210
|
-
export function
|
|
211
|
-
const
|
|
326
|
+
export function currentTransferTables(events) {
|
|
327
|
+
const candidates = new Map();
|
|
328
|
+
const cleared = new Map();
|
|
329
|
+
for (const event of events) {
|
|
330
|
+
if (event.kind !== "engineering_projection") continue;
|
|
331
|
+
const projection = event.payload;
|
|
332
|
+
if (projection?.kind === "projection_clear" && projection.clear_kind === "transfer_table") {
|
|
333
|
+
const ids = cleared.get(projection.node_id) ?? new Set();
|
|
334
|
+
ids.add(projection.execution_id);
|
|
335
|
+
cleared.set(projection.node_id, ids);
|
|
336
|
+
} else if (projection?.kind === "transfer_table") {
|
|
337
|
+
const rows = candidates.get(projection.node_id) ?? [];
|
|
338
|
+
rows.push(projection);
|
|
339
|
+
candidates.set(projection.node_id, rows);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return [...candidates.entries()].flatMap(([node, rows]) => {
|
|
343
|
+
const current = [...rows].reverse().find(row => !cleared.get(node)?.has(row.execution_id));
|
|
344
|
+
return current ? [current] : [];
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function humanRun(value, { projectId, cost, currentTransferIds } = {}) {
|
|
349
|
+
const terminal = value.terminal === "" ? null : terminalPresentation(value);
|
|
212
350
|
const origins = value.events.flatMap(event => event.kind === "step_finished"
|
|
213
351
|
? inputLines(event.payload?.step?.inputs ?? event.payload?.inputs) : []);
|
|
214
352
|
const artifacts = value.events.filter(event => event.kind === "artifact").map(event => event.payload);
|
|
@@ -217,11 +355,25 @@ export function humanRun(value, { projectId, cost } = {}) {
|
|
|
217
355
|
}
|
|
218
356
|
const tasks = taskLines(value.events);
|
|
219
357
|
const assumptions = assumptionLines(value.events);
|
|
358
|
+
const currentTables = currentTransferTables(value.events).filter(projection =>
|
|
359
|
+
currentTransferIds?.has(projection.projection_id));
|
|
220
360
|
const lines = [];
|
|
221
|
-
if (terminal
|
|
361
|
+
if (terminal?.answer !== undefined) lines.push(terminal.answer, "");
|
|
222
362
|
if (tasks.length) lines.push("Steps:", ...tasks, "");
|
|
223
363
|
if (origins.length) lines.push("Inputs and origins:", ...origins, "");
|
|
224
364
|
if (assumptions.length) lines.push("Assumptions:", ...assumptions, "");
|
|
365
|
+
for (const projection of currentTables) {
|
|
366
|
+
if (typeof projection.search_status !== "string" || projection.origin !== "unattributed"
|
|
367
|
+
|| typeof projection.validation_status !== "string" || typeof projection.withheld !== "boolean") {
|
|
368
|
+
throw new CliError("projection_invalid", "Transfer table metadata is malformed");
|
|
369
|
+
}
|
|
370
|
+
lines.push(`Transfer table: ${plainLine(projection.search_status)} · input origins ${projection.origin}`
|
|
371
|
+
+ ` · validation ${plainLine(projection.validation_status)}`
|
|
372
|
+
+ ` · path ${projection.withheld ? "withheld" : "shown"}`);
|
|
373
|
+
const project = projectId ? ` --project ${shellQuote(projectId)}` : "";
|
|
374
|
+
lines.push(` vecteur projection get ${shellQuote(value.run_id)} ${shellQuote(projection.projection_id)}`
|
|
375
|
+
+ ` --output ${shellQuote("transfer-table.json")}${project}`, "");
|
|
376
|
+
}
|
|
225
377
|
if (artifacts.length) {
|
|
226
378
|
lines.push("Artifacts:");
|
|
227
379
|
for (const artifact of artifacts) {
|
|
@@ -231,9 +383,12 @@ export function humanRun(value, { projectId, cost } = {}) {
|
|
|
231
383
|
}
|
|
232
384
|
lines.push("");
|
|
233
385
|
}
|
|
234
|
-
lines.push(`Run: ${plainLine(value.run_id)}
|
|
235
|
-
if (terminal
|
|
236
|
-
|
|
386
|
+
lines.push(`Run: ${plainLine(value.run_id)}`);
|
|
387
|
+
if (terminal) {
|
|
388
|
+
lines.push(`Terminal: ${terminal.state}`);
|
|
389
|
+
if (terminal.defect !== undefined) lines.push(`Cause: ${terminal.defect}`);
|
|
390
|
+
lines.push(...terminal.recovery.map(recoveryLine));
|
|
391
|
+
}
|
|
237
392
|
if (cost) lines.push("", humanCost(cost).trimEnd());
|
|
238
393
|
return `${lines.join("\n")}\n`;
|
|
239
394
|
}
|
package/src/mcp.js
CHANGED
|
@@ -3,7 +3,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
3
3
|
import * as z from "zod/v4";
|
|
4
4
|
|
|
5
5
|
import { CliError, VecteurClient, redact } from "./client.js";
|
|
6
|
-
import { EVENT_KINDS, RETIRED_EVENT_KINDS } from "./contract.js";
|
|
6
|
+
import { EVENT_KINDS, RETIRED_EVENT_KINDS, TERMINAL_STATES } from "./contract.js";
|
|
7
7
|
import { canonicalJson } from "./format.js";
|
|
8
8
|
|
|
9
9
|
const projectInput = z.object({ project_id: z.string().min(1) }).strict();
|
|
@@ -53,9 +53,9 @@ const runControlOutput = z.object({
|
|
|
53
53
|
text: z.string().nullable().optional(),
|
|
54
54
|
}).strict();
|
|
55
55
|
const admittedOutput = z.object({
|
|
56
|
-
run_id: z.string().min(1), state_hash: z.string().min(1), result_hash: z.string()
|
|
56
|
+
run_id: z.string().min(1), state_hash: z.string().min(1), result_hash: z.string(),
|
|
57
57
|
requested_route_id: z.string().min(1), profile: z.string().min(1),
|
|
58
|
-
terminal: z.enum([
|
|
58
|
+
terminal: z.union([z.enum([...TERMINAL_STATES]), z.literal("")]),
|
|
59
59
|
duration_seconds: z.number().int().safe().nonnegative().nullable().optional(),
|
|
60
60
|
usage: z.object({
|
|
61
61
|
coverage: z.enum(["complete", "incomplete"]), provider_id: z.string().min(1),
|
|
@@ -64,7 +64,9 @@ const admittedOutput = z.object({
|
|
|
64
64
|
}).strict(),
|
|
65
65
|
events: z.array(eventOutput),
|
|
66
66
|
controls: z.array(runControlOutput),
|
|
67
|
-
}).strict()
|
|
67
|
+
}).strict().refine(value => value.terminal === ""
|
|
68
|
+
? value.result_hash === "" && value.events.at(-1)?.kind !== "terminal"
|
|
69
|
+
: value.result_hash.length > 0, "Run terminal and result hash are not paired");
|
|
68
70
|
|
|
69
71
|
function success(value) {
|
|
70
72
|
return {
|
|
@@ -114,7 +116,7 @@ export function createVecteurMcpServer(client = new VecteurClient(), shutdownSig
|
|
|
114
116
|
return client.runSubmit(projectId, ask, onEvent, signal(context));
|
|
115
117
|
});
|
|
116
118
|
|
|
117
|
-
tool(server, "vecteur_run_get", "Get one
|
|
119
|
+
tool(server, "vecteur_run_get", "Get one Vecteur run record, including an open run.", runInput, admittedOutput,
|
|
118
120
|
({ run_id: runId }, context) => client.runGet(runId, signal(context)));
|
|
119
121
|
|
|
120
122
|
tool(server, "vecteur_run_events", "Validate run events against its direct record.", runInput,
|