@parcha/agentrun-pi 0.1.0-beta.5 → 0.1.0-beta.9
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.
|
@@ -54,8 +54,10 @@ async function withAdapterDiagnostic(stage, run) {
|
|
|
54
54
|
if (error.code === 'http') {
|
|
55
55
|
if (Number.isInteger(error.status) && error.status >= 100 && error.status <= 599)
|
|
56
56
|
diagnostic.status = error.status;
|
|
57
|
-
if (reason === 'max_tokens_exceeded')
|
|
57
|
+
if (reason === 'max_tokens_exceeded') {
|
|
58
58
|
diagnostic.reason = reason;
|
|
59
|
+
diagnostic.message = 'Jev refused the request: it exceeds the model\'s token budget. Send fewer or shorter items per request. No judgment was accepted; provider details are omitted.';
|
|
60
|
+
}
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -124,7 +126,7 @@ function localDiagnostic(error) {
|
|
|
124
126
|
&& stateBytes > maxStateBytes && maxStateBytes > 0)
|
|
125
127
|
return {
|
|
126
128
|
code: 'jev_state_too_large', stage, stateBytes, maxStateBytes,
|
|
127
|
-
message: `Jev state at ${stage} is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes}. No request was sent. A sift
|
|
129
|
+
message: `Jev state at ${stage} is ${stateBytes} UTF-8 bytes; configured host limit is ${maxStateBytes}. No request was sent. A sift splits its items across requests only when the host sets maxStateBytesPerRequest; otherwise send less of each item or use per-item judgments without losing headers or qualifiers.`
|
|
128
130
|
};
|
|
129
131
|
if (reason === 'invalid_state' || reason === 'invalid_questions')
|
|
130
132
|
return { code: `jev_${reason}`, stage,
|
|
@@ -361,11 +363,36 @@ export class WorkflowExtensionService {
|
|
|
361
363
|
queue.shift()?.();
|
|
362
364
|
}
|
|
363
365
|
};
|
|
366
|
+
const rejectTrace = (error) => {
|
|
367
|
+
rejectedEvents++;
|
|
368
|
+
traceTruncated = true;
|
|
369
|
+
if (!signal.aborted) {
|
|
370
|
+
// Snapshot rejects unsafe/non-JSON values separately from resource bounds.
|
|
371
|
+
// Keep fail-closed cancellation; never echo values or arbitrary error text.
|
|
372
|
+
const diagnostic = error instanceof ExtensionServiceError && error.code === 'data'
|
|
373
|
+
? new ExtensionServiceError('trace_invalid_data', 'Workflow trace contains non-JSON data. Numbers must be finite; objects and arrays must be plain, acyclic and free of accessors or symbols.')
|
|
374
|
+
: error instanceof ExtensionServiceError && error.code === 'bounds'
|
|
375
|
+
? new ExtensionServiceError('limit', 'Workflow trace exceeded its JSON byte, depth or value-count limit.')
|
|
376
|
+
: new ExtensionServiceError('trace_capture_failed', 'Workflow trace capture failed. Unsafe event details are omitted.');
|
|
377
|
+
controller.abort(diagnostic);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const prepareEvent = (event) => {
|
|
381
|
+
if (closed)
|
|
382
|
+
return { type: event.type, label: event.label };
|
|
383
|
+
try {
|
|
384
|
+
return snapshot(event, this.limits.maxEventBytes);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
rejectTrace(error);
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
364
391
|
const onEvent = (event) => {
|
|
365
392
|
if (closed)
|
|
366
393
|
return;
|
|
367
394
|
try {
|
|
368
|
-
const clean = sanitize(
|
|
395
|
+
const clean = sanitize(event);
|
|
369
396
|
const size = Buffer.byteLength(JSON.stringify(clean));
|
|
370
397
|
if (size > this.limits.maxEventBytes)
|
|
371
398
|
throw new ExtensionServiceError('bounds', 'Workflow event exceeds its serialized byte limit.');
|
|
@@ -405,26 +432,17 @@ export class WorkflowExtensionService {
|
|
|
405
432
|
}
|
|
406
433
|
}
|
|
407
434
|
catch (error) {
|
|
408
|
-
|
|
409
|
-
traceTruncated = true;
|
|
410
|
-
if (!signal.aborted) {
|
|
411
|
-
// Snapshot rejects unsafe/non-JSON values separately from resource bounds.
|
|
412
|
-
// Keep fail-closed cancellation; never echo values or arbitrary error text.
|
|
413
|
-
const diagnostic = error instanceof ExtensionServiceError && error.code === 'data'
|
|
414
|
-
? new ExtensionServiceError('trace_invalid_data', 'Workflow trace contains non-JSON data. Numbers must be finite; objects and arrays must be plain, acyclic and free of accessors or symbols.')
|
|
415
|
-
: error instanceof ExtensionServiceError && error.code === 'bounds'
|
|
416
|
-
? new ExtensionServiceError('limit', 'Workflow trace exceeded its JSON byte, depth or value-count limit.')
|
|
417
|
-
: new ExtensionServiceError('trace_capture_failed', 'Workflow trace capture failed. Unsafe event details are omitted.');
|
|
418
|
-
controller.abort(diagnostic);
|
|
419
|
-
}
|
|
435
|
+
rejectTrace(error);
|
|
420
436
|
}
|
|
421
437
|
};
|
|
422
438
|
const sections = Object.entries(this.rubricSections);
|
|
423
439
|
const deps = {
|
|
424
|
-
signal, onEvent,
|
|
440
|
+
signal, onEvent, prepareEvent,
|
|
425
441
|
...(options.deps.skill ? { skill: options.deps.skill } : {}),
|
|
426
442
|
...(sections.length ? { sop: sections.map(([name, text]) => `## ${name}\n${text}`).join('\n\n') } : options.deps.sop ? { sop: options.deps.sop } : {}),
|
|
427
|
-
|
|
443
|
+
// Forward any set limit, so the interpreter rejects an invalid one rather than running without it.
|
|
444
|
+
...(options.deps.maxQuestionsPerRequest !== undefined ? { maxQuestionsPerRequest: options.deps.maxQuestionsPerRequest } : {}),
|
|
445
|
+
...(options.deps.maxStateBytesPerRequest !== undefined ? { maxStateBytesPerRequest: options.deps.maxStateBytesPerRequest } : {}),
|
|
428
446
|
...(options.deps.runNode ? { runNode: params => dispatch('agent', params.signal, () => withAdapterDiagnostic(params.label, () => options.deps.runNode({ ...params, tools: params.tools ?? [...this.allowedTools] })), true) } : {}),
|
|
429
447
|
...(options.deps.runJudge ? { runJudge: params => dispatch('judge', params.signal, () => withAdapterDiagnostic(params.label, () => options.deps.runJudge(params)), true) } : {}),
|
|
430
448
|
...(options.deps.runEffect ? { runEffect: params => dispatch('tool', params.signal, () => {
|
package/dist/runner.js
CHANGED
|
@@ -70,6 +70,9 @@ export function createPiRunner(options) {
|
|
|
70
70
|
for (const name of requested)
|
|
71
71
|
if (!names.has(name))
|
|
72
72
|
throw new Error(`Pi tool was not explicitly registered: ${name}`);
|
|
73
|
+
// Every Pi node starts a fresh session, so there is no caller transcript to fork.
|
|
74
|
+
if (request.context === "fork")
|
|
75
|
+
throw new Error(`Pi node "${request.label}" asks for context: "fork", but the Pi runner starts every node in a fresh session and has no caller transcript to fork. Declare context: "fresh", or run it on a host that supports fork.`);
|
|
73
76
|
const validator = Compile(request.schema);
|
|
74
77
|
const parameters = submitParameters(request.schema);
|
|
75
78
|
const envelopeValidator = Compile(parameters);
|
|
@@ -20,14 +20,14 @@ A workflow is {"v":2,"name","schemas","input"?,"output","root"}.
|
|
|
20
20
|
This is the complete set. Each kind accepts exactly these fields; any other field is an error.
|
|
21
21
|
- `chain`: `steps`
|
|
22
22
|
- `code`: `label`, `code`, `as`
|
|
23
|
-
- `agent`: `label`, `state`, `instructions`, `sopSection`, `out`, `as`, `requires`, `tools`, `effort`, `thinking`, `verify`, `tier`
|
|
23
|
+
- `agent`: `label`, `state`, `instructions`, `sopSection`, `out`, `as`, `requires`, `tools`, `effort`, `thinking`, `verify`, `tier`, `budget_usd`, `context`
|
|
24
24
|
- `decide`: `label`, `state`, `instructions`, `sopSection`, `out`, `as`, `requires`, `tools`, `effort`, `thinking`, `verify`, `tier`
|
|
25
25
|
- `extract`: `label`, `state`, `instructions`, `sopSection`, `out`, `as`, `requires`, `tools`, `effort`, `thinking`, `verify`, `tier`
|
|
26
26
|
- `report`: `label`, `state`, `instructions`, `sopSection`, `requires`, `tools`, `effort`, `thinking`
|
|
27
27
|
- `artifact`: `label`, `state`, `type`, `path`, `instructions`, `sopSection`, `requires`, `tools`, `effort`, `thinking`
|
|
28
28
|
- `map`: `label`, `itemsPath`, `body`, `as`, `resultPath`, `maxConcurrency`
|
|
29
29
|
- `parallel`: `label`, `branches`
|
|
30
|
-
- `loop`: `label`, `body`, `until`, `maxIters`
|
|
30
|
+
- `loop`: `label`, `body`, `until`, `maxIters`, `expect_iters`
|
|
31
31
|
- `escalate`: `label`, `when`, `kind`, `stage`, `summary`
|
|
32
32
|
- `call`: `label`, `via`, `tool`, `args`, `code`, `input`, `command`, `env`, `where`, `out`, `as`, `produces`, `deadline_s`, `retry`, `poll`, `requires`
|
|
33
33
|
- `workflow`: `label`, `workflow`, `input`, `out`, `as`
|
|
@@ -51,9 +51,10 @@ Their fields:
|
|
|
51
51
|
- `out` names the schema the submission must satisfy; the value lands at `as`.
|
|
52
52
|
- `requires` lists state paths that must hold evidence before the node runs, each produced upstream: missing values, null, blank strings, empty arrays and empty objects stop the run; `false` and `0` pass.
|
|
53
53
|
- `tools` names tools the host offers. `[]` means no tools; omitting `tools` offers every tool the host allows. Never name a tool the host did not offer.
|
|
54
|
-
- `effort` is minimal|low|medium|high; `thinking` is low|medium|high (never off); `tier` is fast|default|strong. They are requests to the host: resource ceilings are the host's
|
|
54
|
+
- `effort` is minimal|low|medium|high; `thinking` is low|medium|high (never off); `tier` is fast|default|strong. They are requests to the host: resource ceilings are the host's.
|
|
55
|
+
- An `agent` node may carry `budget_usd`, the most it asks to spend in dollars (above 0), which the host caps by what it has left, and `context`, fresh|fork: `fresh` (the default) sees only its instructions and state; `fork` asks the host to start it from the caller's transcript, which a host refuses where it has none.
|
|
55
56
|
- `sopSection` names one heading of the host's SOP, or a list of them, as the exact text after "## ". The node receives those sections verbatim.
|
|
56
|
-
- `verify` reviews a submission before it is accepted: {"out": a question schema with at least one boolean question, "state"?, "maxDrives"?: 1
|
|
57
|
+
- `verify` reviews a submission before it is accepted: {"out": a question schema with at least one boolean question, "state"?, "maxDrives"?: an integer >= 1 (default 2, no maximum), "override"?: {"below": a number in (0, 1), default 0.3}}. A boolean question named after a submission field doubts that field when its yes-probability is below `override.below`; any other boolean question is a requirement met at 0.5. A rejected submission returns to the same session with the reasons.
|
|
57
58
|
- When the workflow has a report, no decide or extract `out` schema carries `report_markdown`.
|
|
58
59
|
|
|
59
60
|
### Terminal nodes: report and artifact
|
|
@@ -68,7 +69,7 @@ Typed questions answered by the host's judge in one request each: no tools, no s
|
|
|
68
69
|
- A question schema is a flat object. Each property's `description` is its question. A boolean yields a yes-probability; a string enum (at most 240 options, optional per-option `criteria`) yields a choice; an integer with `criteria`: [level descriptions] (2 to 10 levels, minimum 0, maximum the last level index) yields a score. Nothing else is a question.
|
|
69
70
|
- `judge`: a non-empty `state` map, `out` (a question schema) and `as`. The decoded value lands at `as`, the raw answers at `<as>$answers`.
|
|
70
71
|
- `pick`: `itemsPath`, `describe` (the option text per item, such as "{item.name}"), `instructions` (the one question) and `as`; `allowNone` adds a none-of-these option. The result is {index, item, none, option}.
|
|
71
|
-
- `sift`: `itemsPath`, `out` (a question schema asked of every item
|
|
72
|
+
- `sift`: `itemsPath`, `out` (a question schema asked of every item; the host splits the items across requests when they exceed its limits) and `as`. `keep` {path: a question id or <id>.confidence, never a choice, gte?} keeps passing items, in order, at `<as>.items`.
|
|
72
73
|
- `route`: a non-empty `state` map, `instructions` (the one question) and 2 to 240 named `branches`, each {criteria?, body}. `unsure` {branch: one of the branches, gte: a number in (0, 1]} takes that branch when the choice's confidence is below `gte`. `as` records the choice.
|
|
73
74
|
- A decoded boolean is true at yes-probability 0.5. To hold a different threshold, read `<as>$answers.answers.<id>.noul` in a code node.
|
|
74
75
|
|
|
@@ -77,21 +78,22 @@ Typed questions answered by the host's judge in one request each: no tools, no s
|
|
|
77
78
|
- `chain`: non-empty `steps`, run in order.
|
|
78
79
|
- `map`: `itemsPath` (an upstream list), `body` and `as`. The body sees `item` and `item_index`; `maxConcurrency` is a positive integer (default 4); `resultPath` selects one path from each completed item's state.
|
|
79
80
|
- `parallel`: at least two `branches`, each starting from the state before the parallel node. Branches write disjoint keys and never read a sibling's writes.
|
|
80
|
-
- `loop`: `body`, `until` (a predicate) and an integer `maxIters`
|
|
81
|
+
- `loop`: `body`, `until` (a predicate) and an integer `maxIters` of at least 1 (no maximum), optionally an integer `expect_iters` of at least 1 (priced passes). At the bound the state passes through with `until` unmet; follow the loop with an escalate or gate on that condition.
|
|
81
82
|
- `escalate`: `when` (a predicate), and non-empty `kind`, `stage` and `summary`. When the predicate holds the run stops without output and returns the escalation with its interpolated summary.
|
|
82
83
|
- `workflow`: `label`, `workflow` (a complete inline child), `input` (an object, interpolated, the child's entire initial state), `out` (a parent schema checked against the child's output) and `as`. The child declares `input.schemaId` in its own schemas and contains no report or artifact.
|
|
83
84
|
- `code`: `code` is one synchronous function expression such as "(s) => ({ total: s.items.length })". It receives the full state; return new values and never mutate it. `Date`, `Promise`, timers, `fetch`, `require`, `process`, `Function` and `globalThis` are unavailable. It is trusted host JavaScript, not a sandbox. Use code for typed-state mechanics, never to read meaning from prose.
|
|
84
|
-
- `call`: one side effect with no model in the loop. `via` is tool|executor|shell; `as` and `deadline_s` (greater than 0,
|
|
85
|
+
- `call`: one side effect with no model in the loop. `via` is tool|executor|shell; `as` and `deadline_s` (a finite number greater than 0, no maximum) are required — an unbounded effect is how a stalled provider becomes a stalled job.
|
|
85
86
|
- `via: tool` takes `tool` (a host tool address), `args` (an object) and `out` (the tool's result schema).
|
|
86
87
|
- `via: executor` takes `code` (a body that returns its JSON result and uses only `tools` and `input`), `input` and `out`.
|
|
87
88
|
- `via: shell` takes `command` (literal) and `env` (UPPER_CASE names to strings, interpolated by value; the way long values reach a command). Its result has the fixed shape {code, stdout, stderr, truncated?}: no `out`. Only a shell call may declare `produces` (workspace-relative files, checked after the effect).
|
|
88
|
-
- `retry` {attempts: 1
|
|
89
|
-
- `poll` {until, fail_when?, interval_s: 0.1
|
|
89
|
+
- `retry` {attempts: an integer >= 1, backoff_s?: a number >= 0, on?: [timeout, http_5xx, http_429, connection, exit, effect_timeout, effect_exit, effect_transport, effect_not_granted, effect_produces_missing, effect_path_escape, effect_code_unsafe, output_invalid, state_invalid, effect_unknown_transport]}. `where` accepts only "sandbox".
|
|
90
|
+
- `poll` {until, fail_when?, interval_s: >= 0.1 (a busy-loop guard), deadline_s: at least the call's deadline_s (no maximum)} repeats the call until `until` holds on its own result; `fail_when` fails it at once. Both are mechanical predicates whose paths are relative to the result and lie in its declared shape.
|
|
90
91
|
- Validation and dry runs never perform effects: they synthesize the declared result, and a shell result's stdout is "{}".
|
|
91
92
|
|
|
92
93
|
## Predicates
|
|
93
94
|
|
|
94
95
|
`loop.until` and `escalate.when` take one of these; `poll` takes only the first 8. Each path or key reads a state value an input or an earlier node produced.
|
|
96
|
+
A predicate is one JSON object that names itself in its `predicate` field beside its own fields, for example `{"predicate": "field_true", "path": "review.approved"}`.
|
|
95
97
|
- `field_equals` {path, value}: holds when the value at `path` equals `value` (a string, number or boolean)
|
|
96
98
|
- `field_true` {path}: holds when the value at `path` is `true`
|
|
97
99
|
- `in` {path, values}: holds when the value at `path` is one of `values` (a non-empty list of strings, numbers or booleans)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parcha/agentrun-pi",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.9",
|
|
4
4
|
"description": "Pi extension for building, running and evaluating AgentRun workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"@earendil-works/pi-ai": "0.87.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@parcha/agentrun-dsl": "0.1.0-beta.
|
|
39
|
-
"@parcha/agentrun-jev": "0.1.0-beta.
|
|
38
|
+
"@parcha/agentrun-dsl": "0.1.0-beta.9",
|
|
39
|
+
"@parcha/agentrun-jev": "0.1.0-beta.9",
|
|
40
40
|
"@earendil-works/pi-agent-core": "0.87.0",
|
|
41
41
|
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
42
42
|
"@earendil-works/pi-tui": "0.87.0",
|