@proagentstore/cli 0.4.104 → 0.4.106
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InputLedger } from "./input-ledger.js";
|
|
1
2
|
import { runObjectiveBlock } from "./run-objective.js";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { classifyCommand, commandFromToolInput, fillTargetFromResult, toolCallOk, toolResultMark } from "./engine-acts.js";
|
|
@@ -22,8 +23,6 @@ export { defaultStatePath } from "./headless-state.js";
|
|
|
22
23
|
* (refusing new records) would lose the turns that just happened rather than ancient ones.
|
|
23
24
|
*/
|
|
24
25
|
const MAX_PENDING_USAGE = 200;
|
|
25
|
-
/** How many consumed-input ids a session reports (#1019). Re-sent on every snapshot, so bounded; the oldest fall off. */
|
|
26
|
-
export const MAX_CONSUMED_INPUTS = 50;
|
|
27
26
|
/**
|
|
28
27
|
* How many un-drained consequential acts a session holds (#294).
|
|
29
28
|
*
|
|
@@ -102,8 +101,8 @@ export class HeadlessSession {
|
|
|
102
101
|
pendingInputs = [];
|
|
103
102
|
/** The id of the input the current one-shot turn was spawned for (#1019). */
|
|
104
103
|
turnInputId = null;
|
|
105
|
-
/**
|
|
106
|
-
|
|
104
|
+
/** Inputs accepted and consumed, bound to their content and the engine process that read them (#1019). */
|
|
105
|
+
inputs = new InputLedger();
|
|
107
106
|
/**
|
|
108
107
|
* The engine's own last output line of the turn in flight (#545).
|
|
109
108
|
*
|
|
@@ -319,14 +318,11 @@ export class HeadlessSession {
|
|
|
319
318
|
* credit for something it had not read.
|
|
320
319
|
*/
|
|
321
320
|
get consumedInputs() {
|
|
322
|
-
return this.
|
|
321
|
+
return this.inputs.consumedIds;
|
|
323
322
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.consumed.push(id);
|
|
328
|
-
if (this.consumed.length > MAX_CONSUMED_INPUTS)
|
|
329
|
-
this.consumed.shift();
|
|
323
|
+
/** Inputs this session accepted — handed to the engine, whether or not a turn has finished on them. */
|
|
324
|
+
get acceptedInputs() {
|
|
325
|
+
return this.inputs.acceptedIds;
|
|
330
326
|
}
|
|
331
327
|
/** The engine process's pid while one is running — what per-session resource attribution reads (#924). */
|
|
332
328
|
get pid() {
|
|
@@ -427,6 +423,7 @@ export class HeadlessSession {
|
|
|
427
423
|
return;
|
|
428
424
|
// A new process has read none of what the old one was sent, and owns none of its half-read line.
|
|
429
425
|
this.pendingInputs = [];
|
|
426
|
+
this.inputs.newEngine();
|
|
430
427
|
this.buf = "";
|
|
431
428
|
const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
|
|
432
429
|
const proc = spawn(this.cmdBin, args, {
|
|
@@ -518,6 +515,8 @@ export class HeadlessSession {
|
|
|
518
515
|
this.pendingSeed = null;
|
|
519
516
|
if (!this.alive)
|
|
520
517
|
this.start();
|
|
518
|
+
if (opts.inputId)
|
|
519
|
+
this.inputs.accept(opts.inputId, opts.digest ?? ""); // after start(): accepted by the process that will read it
|
|
521
520
|
if (replay)
|
|
522
521
|
this.push("[pags] this engine keeps no memory between turns, so ProAgentStore's record of the conversation so far was delivered with this turn — a reconstruction, not the engine's own memory");
|
|
523
522
|
else if (seed)
|
|
@@ -689,7 +688,7 @@ export class HeadlessSession {
|
|
|
689
688
|
this.turnReport = turnReportFromExit(code, signal, this.turnLastLine, Date.now(), this.turnTail);
|
|
690
689
|
// A turn we killed did not finish reading its input (#1019).
|
|
691
690
|
if (this.turnReport.verdict !== "killed")
|
|
692
|
-
this.
|
|
691
|
+
this.inputs.consume(inputId);
|
|
693
692
|
this.run = "idle";
|
|
694
693
|
this.proc = null;
|
|
695
694
|
});
|
|
@@ -864,7 +863,7 @@ export class HeadlessSession {
|
|
|
864
863
|
// own `is_error` — the same claim, in the words the engine states it in. Without
|
|
865
864
|
// this the field would exist for three engines and silently not for the flagship.
|
|
866
865
|
this.turnReport = turnReportFromResult(ev.isError, failure, Date.now(), this.turnTail);
|
|
867
|
-
this.
|
|
866
|
+
this.inputs.consume(this.pendingInputs.shift() ?? null);
|
|
868
867
|
// The same event that ends the turn also reports what the turn COST (#267). It was
|
|
869
868
|
// parsed and thrown away, which is why Engine spend was absent from the ledger.
|
|
870
869
|
// An errored turn still burned tokens, so this is recorded regardless of is_error.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// What a coding session accepted and consumed, bound to CONTENT and to the engine PROCESS (#1019).
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
/** Bound on each list, as before: enough to reconcile the latest acts, never a growing history. */
|
|
4
|
+
export const MAX_LEDGER_INPUTS = 50;
|
|
5
|
+
/** How many of the newest entries ride every snapshot — a retry reconciles the act it just sent. */
|
|
6
|
+
export const REPORTED_LEDGER_INPUTS = 16;
|
|
7
|
+
const str = (v) => (typeof v === "string" ? v : null);
|
|
8
|
+
/**
|
|
9
|
+
* sha256 of an act's EXECUTABLE content — text, author and objective — as received, never its delivery
|
|
10
|
+
* metadata (id, replay). The same id with different content is a conflict, not a retry. Mirrored by the
|
|
11
|
+
* cloud's `actDigest` (workers/api/src/lib/coding-act-send.ts); the two must agree byte for byte.
|
|
12
|
+
*/
|
|
13
|
+
export function actDigest(a) {
|
|
14
|
+
const o = a.objective;
|
|
15
|
+
const objective = o && typeof o === "object" ? [str(o.runId), str(o.text), str(o.origin?.kind), str(o.origin?.by), Array.isArray(o.constraints) ? o.constraints.filter((c) => typeof c === "string") : []] : null;
|
|
16
|
+
return createHash("sha256").update(JSON.stringify([str(a.text), str(a.author), objective])).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One session's input record. `ledger` names THIS record: an id absent from it proves only that this
|
|
20
|
+
* ledger never accepted it — a different runner, or this session re-created after a runner restart, starts
|
|
21
|
+
* empty and cannot speak for its predecessor. `epoch` names the engine process: a replacement process has
|
|
22
|
+
* read nothing its predecessor read, so consumption is credited to the process that did the reading.
|
|
23
|
+
* Ids are never forgotten across a process change, so a retried id is still refused rather than run twice.
|
|
24
|
+
*/
|
|
25
|
+
export class InputLedger {
|
|
26
|
+
ledger = randomUUID();
|
|
27
|
+
epoch = 0;
|
|
28
|
+
accepted = [];
|
|
29
|
+
consumed = [];
|
|
30
|
+
/** `duplicate`: this id with this content was already accepted. `conflict`: this id, different content. */
|
|
31
|
+
check(id, digest) {
|
|
32
|
+
const hit = this.accepted.find((a) => a.id === id);
|
|
33
|
+
return !hit ? "new" : hit.digest === digest ? "duplicate" : "conflict";
|
|
34
|
+
}
|
|
35
|
+
accept(id, digest) {
|
|
36
|
+
this.accepted.push({ id, digest, epoch: this.epoch });
|
|
37
|
+
if (this.accepted.length > MAX_LEDGER_INPUTS)
|
|
38
|
+
this.accepted.shift();
|
|
39
|
+
}
|
|
40
|
+
consume(id) {
|
|
41
|
+
if (!id)
|
|
42
|
+
return;
|
|
43
|
+
this.consumed.push({ id, epoch: this.epoch });
|
|
44
|
+
if (this.consumed.length > MAX_LEDGER_INPUTS)
|
|
45
|
+
this.consumed.shift();
|
|
46
|
+
}
|
|
47
|
+
/** A new engine process replaced the old one. */
|
|
48
|
+
newEngine() {
|
|
49
|
+
this.epoch++;
|
|
50
|
+
}
|
|
51
|
+
get acceptedIds() {
|
|
52
|
+
return this.accepted.map((a) => a.id);
|
|
53
|
+
}
|
|
54
|
+
get consumedIds() {
|
|
55
|
+
return this.consumed.map((c) => c.id);
|
|
56
|
+
}
|
|
57
|
+
report() {
|
|
58
|
+
return {
|
|
59
|
+
ledger: this.ledger,
|
|
60
|
+
epoch: this.epoch,
|
|
61
|
+
accepted: this.accepted.slice(-REPORTED_LEDGER_INPUTS).map((a) => [a.id, a.digest, a.epoch]),
|
|
62
|
+
consumed: this.consumed.slice(-REPORTED_LEDGER_INPUTS).map((c) => [c.id, c.epoch]),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
// The run's objective, delivered verbatim with a turn and attributed to the platform (#1019).
|
|
2
|
+
const ORIGIN_KINDS = new Set(["account", "supervisor", "trigger", "unverified"]);
|
|
3
|
+
/** Bounds on the constraint lines a run objective may carry: they are labels, never an essay. */
|
|
4
|
+
const MAX_CONSTRAINTS = 20;
|
|
5
|
+
const MAX_CONSTRAINT_CHARS = 500;
|
|
2
6
|
/** A run objective as the cloud sent it, or undefined — narrowed, because any caller can POST here (#1019). */
|
|
3
7
|
export function runObjective(raw) {
|
|
4
8
|
const o = raw;
|
|
5
|
-
|
|
9
|
+
if (!(o && typeof o.runId === "string" && o.runId.length <= 128 && typeof o.text === "string" && o.text.trim()))
|
|
10
|
+
return undefined;
|
|
11
|
+
const origin = o.origin;
|
|
12
|
+
const kind = typeof origin?.kind === "string" && ORIGIN_KINDS.has(origin.kind) ? origin.kind : "unverified";
|
|
13
|
+
// A label longer than a label is not shortened into a different claim — it is dropped.
|
|
14
|
+
const label = typeof origin?.by === "string" ? origin.by.replace(/\s+/g, " ").trim() : "";
|
|
15
|
+
const by = label.length <= 128 ? label : "";
|
|
16
|
+
// Over-long or excess lines are dropped, never shortened: a truncated constraint is a different one.
|
|
17
|
+
const lines = Array.isArray(o.constraints) ? o.constraints.filter((c) => typeof c === "string" && !!c.trim() && c.length <= MAX_CONSTRAINT_CHARS).slice(0, MAX_CONSTRAINTS) : [];
|
|
18
|
+
return { runId: o.runId, text: o.text, origin: by ? { kind, by } : { kind }, ...(lines.length ? { constraints: lines } : {}) };
|
|
19
|
+
}
|
|
20
|
+
/** The attribution sentence: only what the platform verified about the author, never more. */
|
|
21
|
+
function attribution(runId, origin) {
|
|
22
|
+
const by = origin.by ? ` (${origin.by})` : "";
|
|
23
|
+
switch (origin.kind) {
|
|
24
|
+
case "account":
|
|
25
|
+
return `the objective submitted for run ${runId} through the owner's authenticated account${by}, exactly as ProAgentStore accepted it. Who composed the text is not verified.`;
|
|
26
|
+
case "supervisor":
|
|
27
|
+
return `the objective a supervising agent${by} delegated for run ${runId}, exactly as ProAgentStore accepted it. A supervisor's goal is not an instruction from the owner.`;
|
|
28
|
+
case "trigger":
|
|
29
|
+
return `the objective a trigger${by} composed for run ${runId}, exactly as ProAgentStore accepted it — standing wiring plus facts about another run, not a message from the owner.`;
|
|
30
|
+
default:
|
|
31
|
+
return `the objective ProAgentStore accepted for run ${runId}. Who wrote it is not verified.`;
|
|
32
|
+
}
|
|
6
33
|
}
|
|
7
34
|
/**
|
|
8
|
-
* The run's objective as the engine reads it (#1019): verbatim, fenced,
|
|
9
|
-
* the Pilot's instruction that follows is its own turn
|
|
35
|
+
* The run's objective as the engine reads it (#1019): verbatim, fenced, attributed only as far as the
|
|
36
|
+
* platform verified — the Pilot's instruction that follows is its own turn. The owner's standing rules and
|
|
37
|
+
* directives, earlier or later, outrank both, and the constraints in force when this turn was sent are
|
|
38
|
+
* spelled out beneath the fence so the precedence is not only a sentence.
|
|
10
39
|
*/
|
|
11
40
|
export function runObjectiveBlock(o) {
|
|
41
|
+
const constraints = o.constraints?.length
|
|
42
|
+
? ["[pags] CONSTRAINTS IN FORCE NOW — from the owner's rules and directives of this run, however old. Where the objective above conflicts with one, the constraint wins:", ...o.constraints]
|
|
43
|
+
: [];
|
|
12
44
|
return [
|
|
13
|
-
`[pags] RUN OBJECTIVE —
|
|
45
|
+
`[pags] RUN OBJECTIVE — ${attribution(o.runId, o.origin ?? { kind: "unverified" })} It is not the Pilot's instruction, which follows as its own turn. The owner's standing rules and directives — earlier in this run or later — take precedence over it.`,
|
|
14
46
|
"<<<OBJECTIVE",
|
|
15
47
|
o.text,
|
|
16
48
|
"OBJECTIVE>>>",
|
|
49
|
+
...constraints,
|
|
17
50
|
].join("\n");
|
|
18
51
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { actDigest } from "./input-ledger.js";
|
|
1
2
|
import { runObjective } from "./run-objective.js";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
@@ -67,8 +68,13 @@ export class CodingRuntime {
|
|
|
67
68
|
// cloud still never trusts it, because an older runner simply 404s and the policy reports
|
|
68
69
|
// that it asked and was not answered.
|
|
69
70
|
// `coding.run-objective` (#1019): a message may carry the run's objective, delivered verbatim and
|
|
70
|
-
// attributed to the platform.
|
|
71
|
-
|
|
71
|
+
// attributed to the platform. `.v2`: attributed by its VERIFIED origin (run-objective.ts) — a cloud
|
|
72
|
+
// sends the objective only to a v2 runner, so an older one is never made to call it the owner's.
|
|
73
|
+
// `coding.input-idempotent`: a message whose inputId this session already accepted is not run again,
|
|
74
|
+
// and every snapshot lists the accepted ids, so a retry after a lost reply can be reconciled.
|
|
75
|
+
// `.v3` renders the owner's constraints in force with the objective; `coding.engine-identity`: every snapshot
|
|
76
|
+
// names its input ledger and engine process, and an id is deduplicated together with its content (input-ledger.ts).
|
|
77
|
+
return ["coding.sessions", "coding.stream", "human.takeover", "coding.inspect", "coding.repo-write", "coding.run-objective", "coding.run-objective.v2", "coding.run-objective.v3", "coding.input-idempotent", "coding.engine-identity"];
|
|
72
78
|
}
|
|
73
79
|
/**
|
|
74
80
|
* Resolve the workDir for a read-only inspection. Prefer the tracked session's real
|
|
@@ -244,6 +250,8 @@ export class CodingRuntime {
|
|
|
244
250
|
// the omitted-when-null shape keeps "not measured" distinguishable from a verdict.
|
|
245
251
|
...(session.lastTurn ? { lastTurn: session.lastTurn } : {}),
|
|
246
252
|
...(session.consumedInputs.length ? { consumedInputs: [...session.consumedInputs] } : {}),
|
|
253
|
+
acceptedInputs: [...session.acceptedInputs],
|
|
254
|
+
engine: session.inputs.report(),
|
|
247
255
|
...(opts.drainUsage ? { usage: session.takeUsage(), acts: session.takeActs() } : {}),
|
|
248
256
|
};
|
|
249
257
|
}
|
|
@@ -251,16 +259,25 @@ export class CodingRuntime {
|
|
|
251
259
|
act(sessionId, action) {
|
|
252
260
|
const session = this.require(sessionId);
|
|
253
261
|
switch (action.kind) {
|
|
254
|
-
case "message":
|
|
262
|
+
case "message": {
|
|
263
|
+
const inputId = typeof action.inputId === "string" && action.inputId.length <= 128 ? action.inputId : undefined;
|
|
264
|
+
// The same input twice is a retry after a lost reply, not a second instruction (#1019) — but only with
|
|
265
|
+
// the same content: a changed payload under an accepted id is refused, never credited as the first.
|
|
266
|
+
const digest = actDigest(action);
|
|
267
|
+
const seen = inputId ? session.inputs.check(inputId, digest) : "new";
|
|
268
|
+
if (seen !== "new")
|
|
269
|
+
return { ...this.snapshot(sessionId), ...(seen === "duplicate" ? { duplicateInput: inputId } : { inputConflict: inputId }) };
|
|
255
270
|
// Narrowed rather than trusted: this runner is a published package any caller can
|
|
256
271
|
// POST to, and an unrecognised author must read as "unstated", not become a label.
|
|
257
272
|
session.input(action.text, {
|
|
258
273
|
author: asTurnAuthor(action.author),
|
|
259
274
|
replay: typeof action.replay === "string" ? action.replay : undefined,
|
|
260
|
-
inputId
|
|
275
|
+
inputId,
|
|
276
|
+
digest,
|
|
261
277
|
objective: runObjective(action.objective),
|
|
262
278
|
});
|
|
263
279
|
break;
|
|
280
|
+
}
|
|
264
281
|
case "keys": {
|
|
265
282
|
// A snapshot is no longer the whole answer (#448). `key()` records the attempt and
|
|
266
283
|
// reports that it was not delivered; answering 200 with a pane that simply did not
|