@proagentstore/cli 0.4.105 → 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,10 +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
|
-
|
|
107
|
-
/** Ids of inputs handed to the engine, newest last and bounded (#1019): what a retry is reconciled against. */
|
|
108
|
-
accepted = [];
|
|
104
|
+
/** Inputs accepted and consumed, bound to their content and the engine process that read them (#1019). */
|
|
105
|
+
inputs = new InputLedger();
|
|
109
106
|
/**
|
|
110
107
|
* The engine's own last output line of the turn in flight (#545).
|
|
111
108
|
*
|
|
@@ -321,18 +318,11 @@ export class HeadlessSession {
|
|
|
321
318
|
* credit for something it had not read.
|
|
322
319
|
*/
|
|
323
320
|
get consumedInputs() {
|
|
324
|
-
return this.
|
|
321
|
+
return this.inputs.consumedIds;
|
|
325
322
|
}
|
|
326
323
|
/** Inputs this session accepted — handed to the engine, whether or not a turn has finished on them. */
|
|
327
324
|
get acceptedInputs() {
|
|
328
|
-
return this.
|
|
329
|
-
}
|
|
330
|
-
credit(id) {
|
|
331
|
-
if (!id)
|
|
332
|
-
return;
|
|
333
|
-
this.consumed.push(id);
|
|
334
|
-
if (this.consumed.length > MAX_CONSUMED_INPUTS)
|
|
335
|
-
this.consumed.shift();
|
|
325
|
+
return this.inputs.acceptedIds;
|
|
336
326
|
}
|
|
337
327
|
/** The engine process's pid while one is running — what per-session resource attribution reads (#924). */
|
|
338
328
|
get pid() {
|
|
@@ -433,6 +423,7 @@ export class HeadlessSession {
|
|
|
433
423
|
return;
|
|
434
424
|
// A new process has read none of what the old one was sent, and owns none of its half-read line.
|
|
435
425
|
this.pendingInputs = [];
|
|
426
|
+
this.inputs.newEngine();
|
|
436
427
|
this.buf = "";
|
|
437
428
|
const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
|
|
438
429
|
const proc = spawn(this.cmdBin, args, {
|
|
@@ -512,11 +503,6 @@ export class HeadlessSession {
|
|
|
512
503
|
* session-start brief, being the same record composed later.
|
|
513
504
|
*/
|
|
514
505
|
input(text, opts = {}) {
|
|
515
|
-
if (opts.inputId) {
|
|
516
|
-
this.accepted.push(opts.inputId);
|
|
517
|
-
if (this.accepted.length > MAX_CONSUMED_INPUTS)
|
|
518
|
-
this.accepted.shift();
|
|
519
|
-
}
|
|
520
506
|
// The Engine reads the preamble, the pane the short marker; the instruction stays evidence.
|
|
521
507
|
const sent = authoredTurn(text, opts.author);
|
|
522
508
|
// The brief leads the turn and is spent in the reading (#693). It goes to the ENGINE only:
|
|
@@ -529,6 +515,8 @@ export class HeadlessSession {
|
|
|
529
515
|
this.pendingSeed = null;
|
|
530
516
|
if (!this.alive)
|
|
531
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
|
|
532
520
|
if (replay)
|
|
533
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");
|
|
534
522
|
else if (seed)
|
|
@@ -700,7 +688,7 @@ export class HeadlessSession {
|
|
|
700
688
|
this.turnReport = turnReportFromExit(code, signal, this.turnLastLine, Date.now(), this.turnTail);
|
|
701
689
|
// A turn we killed did not finish reading its input (#1019).
|
|
702
690
|
if (this.turnReport.verdict !== "killed")
|
|
703
|
-
this.
|
|
691
|
+
this.inputs.consume(inputId);
|
|
704
692
|
this.run = "idle";
|
|
705
693
|
this.proc = null;
|
|
706
694
|
});
|
|
@@ -875,7 +863,7 @@ export class HeadlessSession {
|
|
|
875
863
|
// own `is_error` — the same claim, in the words the engine states it in. Without
|
|
876
864
|
// this the field would exist for three engines and silently not for the flagship.
|
|
877
865
|
this.turnReport = turnReportFromResult(ev.isError, failure, Date.now(), this.turnTail);
|
|
878
|
-
this.
|
|
866
|
+
this.inputs.consume(this.pendingInputs.shift() ?? null);
|
|
879
867
|
// The same event that ends the turn also reports what the turn COST (#267). It was
|
|
880
868
|
// parsed and thrown away, which is why Engine spend was absent from the ledger.
|
|
881
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,5 +1,8 @@
|
|
|
1
1
|
// The run's objective, delivered verbatim with a turn and attributed to the platform (#1019).
|
|
2
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;
|
|
3
6
|
/** A run objective as the cloud sent it, or undefined — narrowed, because any caller can POST here (#1019). */
|
|
4
7
|
export function runObjective(raw) {
|
|
5
8
|
const o = raw;
|
|
@@ -10,7 +13,9 @@ export function runObjective(raw) {
|
|
|
10
13
|
// A label longer than a label is not shortened into a different claim — it is dropped.
|
|
11
14
|
const label = typeof origin?.by === "string" ? origin.by.replace(/\s+/g, " ").trim() : "";
|
|
12
15
|
const by = label.length <= 128 ? label : "";
|
|
13
|
-
|
|
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 } : {}) };
|
|
14
19
|
}
|
|
15
20
|
/** The attribution sentence: only what the platform verified about the author, never more. */
|
|
16
21
|
function attribution(runId, origin) {
|
|
@@ -28,14 +33,19 @@ function attribution(runId, origin) {
|
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* The run's objective as the engine reads it (#1019): verbatim, fenced, attributed only as far as the
|
|
31
|
-
* platform verified — the Pilot's instruction that follows is its own turn
|
|
32
|
-
* later outrank both
|
|
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.
|
|
33
39
|
*/
|
|
34
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
|
+
: [];
|
|
35
44
|
return [
|
|
36
|
-
`[pags] RUN OBJECTIVE — ${attribution(o.runId, o.origin ?? { kind: "unverified" })} It is not the Pilot's instruction, which follows as its own turn.
|
|
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.`,
|
|
37
46
|
"<<<OBJECTIVE",
|
|
38
47
|
o.text,
|
|
39
48
|
"OBJECTIVE>>>",
|
|
49
|
+
...constraints,
|
|
40
50
|
].join("\n");
|
|
41
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";
|
|
@@ -71,7 +72,9 @@ export class CodingRuntime {
|
|
|
71
72
|
// sends the objective only to a v2 runner, so an older one is never made to call it the owner's.
|
|
72
73
|
// `coding.input-idempotent`: a message whose inputId this session already accepted is not run again,
|
|
73
74
|
// and every snapshot lists the accepted ids, so a retry after a lost reply can be reconciled.
|
|
74
|
-
|
|
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"];
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
80
|
* Resolve the workDir for a read-only inspection. Prefer the tracked session's real
|
|
@@ -248,6 +251,7 @@ export class CodingRuntime {
|
|
|
248
251
|
...(session.lastTurn ? { lastTurn: session.lastTurn } : {}),
|
|
249
252
|
...(session.consumedInputs.length ? { consumedInputs: [...session.consumedInputs] } : {}),
|
|
250
253
|
acceptedInputs: [...session.acceptedInputs],
|
|
254
|
+
engine: session.inputs.report(),
|
|
251
255
|
...(opts.drainUsage ? { usage: session.takeUsage(), acts: session.takeActs() } : {}),
|
|
252
256
|
};
|
|
253
257
|
}
|
|
@@ -257,15 +261,19 @@ export class CodingRuntime {
|
|
|
257
261
|
switch (action.kind) {
|
|
258
262
|
case "message": {
|
|
259
263
|
const inputId = typeof action.inputId === "string" && action.inputId.length <= 128 ? action.inputId : undefined;
|
|
260
|
-
// The same input twice is a retry after a lost reply, not a second instruction (#1019)
|
|
261
|
-
|
|
262
|
-
|
|
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 }) };
|
|
263
270
|
// Narrowed rather than trusted: this runner is a published package any caller can
|
|
264
271
|
// POST to, and an unrecognised author must read as "unstated", not become a label.
|
|
265
272
|
session.input(action.text, {
|
|
266
273
|
author: asTurnAuthor(action.author),
|
|
267
274
|
replay: typeof action.replay === "string" ? action.replay : undefined,
|
|
268
275
|
inputId,
|
|
276
|
+
digest,
|
|
269
277
|
objective: runObjective(action.objective),
|
|
270
278
|
});
|
|
271
279
|
break;
|