@north-light/crouter 0.3.282 → 0.3.284
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/api/client.d.ts +22 -4
- package/dist/api/client.js +34 -0
- package/dist/api/dto/broker-ops.d.ts +11 -0
- package/dist/api/dto/broker.d.ts +3 -8
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/messages.d.ts +2 -2
- package/dist/api/dto/node-outcomes.d.ts +80 -0
- package/dist/api/dto/node-outcomes.js +2 -0
- package/dist/api/dto/nodes.d.ts +9 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/dto/reports.d.ts +45 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +24 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +31 -4
- package/dist/clients/attach/overlays/graph.d.ts +5 -0
- package/dist/clients/attach/overlays/graph.js +65 -47
- package/dist/clients/attach/overlays/pickers.js +8 -9
- package/dist/clients/attach/render/card-presentation.js +1 -1
- package/dist/clients/attach/viewer.js +1286 -1260
- package/dist/commands/api-client.js +22 -23
- package/dist/commands/node/create.js +38 -3
- package/dist/commands/node/inspect.js +3 -7
- package/dist/commands/node/message.js +3 -3
- package/dist/commands/node/outcome.d.ts +2 -0
- package/dist/commands/node/outcome.js +134 -0
- package/dist/commands/node.js +2 -1
- package/dist/commands/profile/pause.js +17 -14
- package/dist/commands/push.js +102 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -1
- package/dist/commands/sys/panels/broker-limits-panel.js +7 -1
- package/dist/commands/sys/panels/models-panel.js +2 -2
- package/dist/commands/sys/settings-shell.d.ts +1 -1
- package/dist/commands/sys/settings-shell.js +6 -0
- package/dist/commands/sys/settings.js +3 -3
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +42 -4
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +9 -0
- package/dist/core/__tests__/integration/profile-pause.test.d.ts +1 -0
- package/dist/core/__tests__/integration/profile-pause.test.js +115 -0
- package/dist/core/__tests__/node-outcome.test.d.ts +1 -0
- package/dist/core/__tests__/node-outcome.test.js +184 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +13 -20
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +6 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +3 -3
- package/dist/core/__tests__/seam/node-outcome-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/seam/node-outcome-delivery.test.js +153 -0
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +22 -0
- package/dist/core/canvas/canvas.d.ts +41 -3
- package/dist/core/canvas/canvas.js +153 -39
- package/dist/core/canvas/crons.js +2 -1
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/migrations.js +47 -0
- package/dist/core/canvas/node-outcome-deliveries.d.ts +80 -0
- package/dist/core/canvas/node-outcome-deliveries.js +163 -0
- package/dist/core/canvas/pid.d.ts +7 -0
- package/dist/core/canvas/pid.js +17 -0
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +42 -1
- package/dist/core/canvas/types.js +2 -1
- package/dist/core/config.d.ts +4 -1
- package/dist/core/config.js +25 -1
- package/dist/core/feed/feed.d.ts +8 -0
- package/dist/core/feed/feed.js +25 -4
- package/dist/core/feed/inbox.d.ts +5 -0
- package/dist/core/feed/inbox.js +13 -0
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +3 -0
- package/dist/core/profiles/manifest.d.ts +3 -1
- package/dist/core/profiles/manifest.js +6 -1
- package/dist/core/runtime/broker/daemon-ops.d.ts +3 -1
- package/dist/core/runtime/broker/daemon-ops.js +6 -0
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -3
- package/dist/core/runtime/broker/engine-drive.js +74 -27
- package/dist/core/runtime/broker/frame-dispatch.js +1 -0
- package/dist/core/runtime/broker/read-ops.js +14 -0
- package/dist/core/runtime/broker/rebind.js +0 -7
- package/dist/core/runtime/broker-protocol.d.ts +1 -6
- package/dist/core/runtime/fleet.d.ts +7 -0
- package/dist/core/runtime/headless-pi.d.ts +13 -0
- package/dist/core/runtime/headless-pi.js +10 -2
- package/dist/core/runtime/lifecycle.d.ts +6 -1
- package/dist/core/runtime/lifecycle.js +50 -1
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +2 -1
- package/dist/core/runtime/outcome-document.d.ts +70 -0
- package/dist/core/runtime/outcome-document.js +97 -0
- package/dist/core/runtime/revive.js +16 -21
- package/dist/core/runtime/spawn.d.ts +5 -1
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/stamp/channel.d.ts +3 -5
- package/dist/core/runtime/stamp/channel.js +3 -10
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/structured-output.d.ts +24 -3
- package/dist/core/runtime/structured-output.js +42 -3
- package/dist/core/runtime/tmux-bindings.js +4 -0
- package/dist/core/runtime/warm-pool.d.ts +5 -2
- package/dist/core/runtime/warm-pool.js +5 -5
- package/dist/core/subscription-state.js +7 -5
- package/dist/core/substrate/render.js +10 -10
- package/dist/core/user-settings.d.ts +18 -1
- package/dist/core/user-settings.js +37 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.js +76 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +122 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.js +86 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +86 -18
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +10 -1
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.js +159 -0
- package/dist/daemon/api/__tests__/submit-result.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/submit-result.test.js +214 -0
- package/dist/daemon/api/handlers/broker-ops.js +53 -24
- package/dist/daemon/api/handlers/canvas.js +2 -2
- package/dist/daemon/api/handlers/messages.js +12 -2
- package/dist/daemon/api/handlers/node-outcomes.d.ts +2 -0
- package/dist/daemon/api/handlers/node-outcomes.js +136 -0
- package/dist/daemon/api/handlers/nodes.js +67 -1
- package/dist/daemon/api/handlers/profiles.js +53 -1
- package/dist/daemon/api/handlers/reports.js +197 -33
- package/dist/daemon/api/map.d.ts +7 -2
- package/dist/daemon/api/map.js +78 -4
- package/dist/daemon/api/server.js +74 -7
- package/dist/daemon/crtrd.js +12 -0
- package/dist/daemon/fleet.d.ts +4 -8
- package/dist/daemon/fleet.js +48 -9
- package/dist/daemon/messaging/node-message.js +2 -1
- package/dist/daemon/node-outcome/deliver-outcome.d.ts +14 -0
- package/dist/daemon/node-outcome/deliver-outcome.js +155 -0
- package/dist/daemon/park-activity.d.ts +9 -0
- package/dist/daemon/park-activity.js +30 -0
- package/dist/daemon/park-pending.d.ts +10 -5
- package/dist/daemon/park-pending.js +18 -8
- package/dist/daemon/reconcilers/bash-deadline.js +6 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +3 -7
- package/dist/daemon/reconcilers/broker-supervision.js +29 -33
- package/dist/daemon/reconcilers/node-deadline.d.ts +5 -0
- package/dist/daemon/reconcilers/node-deadline.js +82 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -1
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +12 -3
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +66 -66
- package/dist/pi-extensions/canvas-structured-output.d.ts +9 -5
- package/dist/pi-extensions/canvas-structured-output.js +23 -297
- package/dist/shared/env.d.ts +8 -0
- package/dist/shared/env.js +14 -0
- package/dist/shared/generated-context.d.ts +3 -9
- package/dist/shared/generated-context.js +8 -16
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/package.json +9 -8
- package/runtime.lock.json +227 -383
|
@@ -137,18 +137,8 @@ export function cliClient(opts) {
|
|
|
137
137
|
// NodeSummaryDTO now carries every NodeRow field and NodeDetailDTO extends it,
|
|
138
138
|
// so the viewer adapter returns the daemon's typed projections directly. It
|
|
139
139
|
// performs no lossy legacy reconstruction or invented local-presence values.
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// visited node on every poll tick, and a `GET /v1/nodes/:id` per call just to
|
|
143
|
-
// read the edge list off the detail DTO costs one round trip per visited node.
|
|
144
|
-
// This short-TTL cache backs those two methods with the lean roster
|
|
145
|
-
// (`GET /v1/canvas/roster`) instead — one shared fetch per TTL window covers
|
|
146
|
-
// every topology walk in that window, comfortably under the attach viewer's 5s
|
|
147
|
-
// poll cadence. Scoped to subscriptionsOf/subscribersOf only: listNodes/getRow
|
|
148
|
-
// stay on the full per-id/list HTTP path because the roster does not carry the
|
|
149
|
-
// runtime fields (intent/waiting_for/pi_pid/etc) those two already populate for
|
|
150
|
-
// real from the full DTO — backing them with the roster would silently null out
|
|
151
|
-
// live display fields, not just the already-lossy tmux-presence ones.
|
|
140
|
+
// Index the short-lived lean roster once so repeated topology lookups stay O(1).
|
|
141
|
+
// listNodes/getRow still use full DTO endpoints for fields the roster omits.
|
|
152
142
|
const ROSTER_CACHE_TTL_MS = 2000;
|
|
153
143
|
export class ApiCanvasSource {
|
|
154
144
|
client;
|
|
@@ -160,15 +150,30 @@ export class ApiCanvasSource {
|
|
|
160
150
|
async roster() {
|
|
161
151
|
const now = Date.now();
|
|
162
152
|
if (this.rosterCache !== null && now - this.rosterCache.at < ROSTER_CACHE_TTL_MS) {
|
|
163
|
-
return this.rosterCache.
|
|
153
|
+
return this.rosterCache.index;
|
|
164
154
|
}
|
|
165
155
|
if (this.rosterInFlight !== null)
|
|
166
156
|
return this.rosterInFlight;
|
|
167
157
|
this.rosterInFlight = this.client
|
|
168
158
|
.canvasRoster()
|
|
169
159
|
.then((roster) => {
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
const index = { subscriptions: new Map(), subscribers: new Map() };
|
|
161
|
+
for (const edge of roster.edges) {
|
|
162
|
+
const subscription = { node_id: edge.to_id, active: edge.active, created: edge.created };
|
|
163
|
+
const subscriber = { node_id: edge.from_id, active: edge.active, created: edge.created };
|
|
164
|
+
const subscriptions = index.subscriptions.get(edge.from_id);
|
|
165
|
+
if (subscriptions === undefined)
|
|
166
|
+
index.subscriptions.set(edge.from_id, [subscription]);
|
|
167
|
+
else
|
|
168
|
+
subscriptions.push(subscription);
|
|
169
|
+
const subscribers = index.subscribers.get(edge.to_id);
|
|
170
|
+
if (subscribers === undefined)
|
|
171
|
+
index.subscribers.set(edge.to_id, [subscriber]);
|
|
172
|
+
else
|
|
173
|
+
subscribers.push(subscriber);
|
|
174
|
+
}
|
|
175
|
+
this.rosterCache = { at: Date.now(), index };
|
|
176
|
+
return index;
|
|
172
177
|
})
|
|
173
178
|
.finally(() => {
|
|
174
179
|
this.rosterInFlight = null;
|
|
@@ -212,16 +217,10 @@ export class ApiCanvasSource {
|
|
|
212
217
|
return rows;
|
|
213
218
|
}
|
|
214
219
|
async subscriptionsOf(subscriber) {
|
|
215
|
-
|
|
216
|
-
return roster.edges
|
|
217
|
-
.filter((e) => e.from_id === subscriber)
|
|
218
|
-
.map((e) => ({ node_id: e.to_id, active: e.active, created: e.created }));
|
|
220
|
+
return this.roster().then((index) => index.subscriptions.get(subscriber) ?? []);
|
|
219
221
|
}
|
|
220
222
|
async subscribersOf(publisher) {
|
|
221
|
-
|
|
222
|
-
return roster.edges
|
|
223
|
-
.filter((e) => e.to_id === publisher)
|
|
224
|
-
.map((e) => ({ node_id: e.from_id, active: e.active, created: e.created }));
|
|
223
|
+
return this.roster().then((index) => index.subscribers.get(publisher) ?? []);
|
|
225
224
|
}
|
|
226
225
|
/** BFS over the active `subscribes_to` edges from `root` (its transitive view),
|
|
227
226
|
* excluding `root` itself — the same traversal shape as
|
|
@@ -6,6 +6,7 @@ import { mark } from '../../core/timing.js';
|
|
|
6
6
|
import { OUTPUT_SCHEMA_CONSTRAINTS, OUTPUT_SCHEMA_TRANSPORT, parseOutputSchemaValue } from '../../core/runtime/structured-output.js';
|
|
7
7
|
import { MODEL_SPEC_FORMS, MODEL_SPEC_NEXT } from '../../core/runtime/model-selection.js';
|
|
8
8
|
import { readMergedLaunchConfig } from '../../core/config.js';
|
|
9
|
+
import { parseDurationMs } from '../../core/wake.js';
|
|
9
10
|
import { profilesStateBlock } from '../../core/profiles/state-block.js';
|
|
10
11
|
import { resolveProfileOperand } from '../../core/profiles/manifest.js';
|
|
11
12
|
import { stateBlock } from '../../core/help.js';
|
|
@@ -14,7 +15,7 @@ import { inTmux } from '../../core/runtime/placement-tmux.js';
|
|
|
14
15
|
import { openSpawnViewer } from '../surface/node/placement.js';
|
|
15
16
|
import { resolve } from 'node:path';
|
|
16
17
|
const YIELD_NUDGE_THRESHOLD = 100_000;
|
|
17
|
-
const NODE_NEW_OUTPUT_SCHEMA_EFFECT = '--output-schema writes the accepted result to the node’s context/result.json and pushes it as the node’s final report.';
|
|
18
|
+
const NODE_NEW_OUTPUT_SCHEMA_EFFECT = '--output-schema writes the accepted result to the node’s context/result.json and pushes it as the node’s final report; a node that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token>`, which finishes it with a failure outcome (reason `declined`) whose `declined` field carries { reason, code, retryable } and no result. Name the code tokens you accept in the prompt; crouter carries the code verbatim and you validate it.';
|
|
18
19
|
const STD_CHILD_FOLLOW_UP = "Do not wait, poll, or duplicate this child's assignment in your own context — there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn — the wake brings you back.";
|
|
19
20
|
/** Decision road sign for a managed (non-root) child. Normally STD_CHILD_FOLLOW_UP,
|
|
20
21
|
* but when the SPAWNER is an orchestrator whose context has already grown past
|
|
@@ -121,6 +122,9 @@ function nodeNewParams() {
|
|
|
121
122
|
// this flag at exactly the moment it is needed. Advertising it would tax
|
|
122
123
|
// every spawn with a capability almost no caller wants.
|
|
123
124
|
{ kind: 'flag', name: 'no-kickoff', type: 'bool', required: false, hidden: true, constraint: 'With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator.' },
|
|
125
|
+
{ kind: 'flag', name: 'deadline', type: 'string', required: false, constraint: 'Positive, representable wall-clock duration from spawn. Grammar: one or more positive integer-unit parts; units: s, m, h, d; adjacent parts are summed. Omit for no deadline.' },
|
|
126
|
+
{ kind: 'flag', name: 'outcome-action', type: 'string', required: false, constraint: 'Declared human-action name that receives the node outcome. Grammar: 1–128 ASCII letters, digits, dots, underscores, or hyphens; starts with a letter or digit. Omit when no external delivery target is needed.' },
|
|
127
|
+
{ kind: 'flag', name: 'outcome-payload', type: 'string', required: false, constraint: 'JSON value, serialized at most 64 KiB, frozen and echoed to --outcome-action. Requires --outcome-action.' },
|
|
124
128
|
{
|
|
125
129
|
kind: 'flag',
|
|
126
130
|
name: 'output-schema',
|
|
@@ -131,10 +135,10 @@ function nodeNewParams() {
|
|
|
131
135
|
whenToUse: 'Use only when the node’s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.',
|
|
132
136
|
value: `Pass the JSON Schema the result must satisfy. ${OUTPUT_SCHEMA_TRANSPORT} ${OUTPUT_SCHEMA_CONSTRAINTS}`,
|
|
133
137
|
effects: [
|
|
134
|
-
'The node
|
|
138
|
+
'The node must answer with `crtr push result` and cannot finish until the result is accepted; a plain `crtr push final` is blocked while the request is pending.',
|
|
135
139
|
NODE_NEW_OUTPUT_SCHEMA_EFFECT,
|
|
136
140
|
'The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.',
|
|
137
|
-
'
|
|
141
|
+
'An accepted result, or a decline, removes the request and terminates the node.',
|
|
138
142
|
],
|
|
139
143
|
},
|
|
140
144
|
},
|
|
@@ -164,6 +168,8 @@ function nodeNewEffects() {
|
|
|
164
168
|
'A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.',
|
|
165
169
|
'--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.',
|
|
166
170
|
NODE_NEW_OUTPUT_SCHEMA_EFFECT,
|
|
171
|
+
'--deadline persists a wall-clock deadline from spawn; expiry cancels the node and records a failure outcome with reason deadline_exceeded.',
|
|
172
|
+
'--outcome-action registers one resolved local human-action target. When the node settles, crouter retries delivery of its outcome document until the target accepts it or permanently rejects it; --outcome-payload is frozen and echoed in that document.',
|
|
167
173
|
...(inTmux() ? ['--root opens a viewer for the new node in the current tmux session.'] : []),
|
|
168
174
|
];
|
|
169
175
|
}
|
|
@@ -203,6 +209,28 @@ async function runNodeCreation(input) {
|
|
|
203
209
|
const situationalContext = situationalContextRaw !== undefined && situationalContextRaw !== '' ? situationalContextRaw : undefined;
|
|
204
210
|
const outputSchemaParsed = parseOutputSchemaValue(input['outputSchema']);
|
|
205
211
|
const outputSchema = outputSchemaParsed !== null ? JSON.stringify(outputSchemaParsed) : null;
|
|
212
|
+
const deadline = input['deadline'];
|
|
213
|
+
if (deadline !== undefined) {
|
|
214
|
+
const durationMs = parseDurationMs(deadline);
|
|
215
|
+
const deadlineAt = durationMs === null ? Number.NaN : new Date(Date.now() + durationMs).getTime();
|
|
216
|
+
if (durationMs === null || !Number.isFinite(durationMs) || durationMs <= 0 || !Number.isFinite(deadlineAt)) {
|
|
217
|
+
throw new InputError({ error: 'invalid_deadline', message: '--deadline must be a positive representable duration', field: 'deadline', next: 'Pass positive integer-unit parts using s, m, h, or d.' });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const outcomeAction = input['outcomeAction'];
|
|
221
|
+
const outcomePayloadRaw = input['outcomePayload'];
|
|
222
|
+
if (outcomePayloadRaw !== undefined && outcomeAction === undefined) {
|
|
223
|
+
throw new InputError({ error: 'outcome_payload_requires_action', message: '--outcome-payload requires --outcome-action', field: 'outcome-payload', next: 'Pass the declared delivery action with --outcome-action.' });
|
|
224
|
+
}
|
|
225
|
+
let outcomePayload;
|
|
226
|
+
if (outcomePayloadRaw !== undefined) {
|
|
227
|
+
try {
|
|
228
|
+
outcomePayload = JSON.parse(outcomePayloadRaw);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
throw new InputError({ error: 'invalid_outcome_payload', message: '--outcome-payload must be valid JSON', field: 'outcome-payload', next: 'Pass one valid JSON value.' });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
206
234
|
const nodeId = input['nodeId']?.trim();
|
|
207
235
|
if (root && worktreeBase !== undefined) {
|
|
208
236
|
throw new InputError({ error: 'worktree_root_conflict', message: '--worktree is mutually exclusive with --root', field: 'worktree', next: 'Drop either --root or --worktree.' });
|
|
@@ -236,6 +264,13 @@ async function runNodeCreation(input) {
|
|
|
236
264
|
...(situationalContext !== undefined ? { situational_context: situationalContext } : {}),
|
|
237
265
|
...(noKickoff ? { no_kickoff: true } : {}),
|
|
238
266
|
...(outputSchema !== null ? { output_schema: outputSchema } : {}),
|
|
267
|
+
...(deadline !== undefined ? { deadline } : {}),
|
|
268
|
+
...(outcomeAction !== undefined ? {
|
|
269
|
+
outcome_delivery: {
|
|
270
|
+
action: outcomeAction,
|
|
271
|
+
...(outcomePayload === undefined ? {} : { payload: outcomePayload }),
|
|
272
|
+
},
|
|
273
|
+
} : {}),
|
|
239
274
|
...(nodeId !== undefined && nodeId !== '' ? { node_id: nodeId } : {}),
|
|
240
275
|
// A bare `--root --no-kickoff` create is exactly what the warm pool holds
|
|
241
276
|
// ready; every other shape is rejected server-side and cold-spawns.
|
|
@@ -82,13 +82,9 @@ const nodeList = defineLeaf({
|
|
|
82
82
|
return false;
|
|
83
83
|
return true;
|
|
84
84
|
});
|
|
85
|
-
const nodes =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const named = { ...row, name: fullName(detail ?? row) };
|
|
89
|
-
if (name === undefined || name === '' || fuzzyMatch(name, named.name))
|
|
90
|
-
nodes.push(named);
|
|
91
|
-
}
|
|
85
|
+
const nodes = rows
|
|
86
|
+
.map((row) => ({ ...row, name: fullName(row) }))
|
|
87
|
+
.filter((row) => name === undefined || name === '' || fuzzyMatch(name, row.name));
|
|
92
88
|
return { nodes };
|
|
93
89
|
},
|
|
94
90
|
});
|
|
@@ -57,7 +57,7 @@ function messageRequestParams() {
|
|
|
57
57
|
{ kind: 'flag', name: 'to', type: 'string', required: false, constraint: 'Target an existing node by id. Exactly one of --to or --self is required.' },
|
|
58
58
|
{ kind: 'flag', name: 'self', type: 'bool', required: false, constraint: 'Target the calling node. Exactly one of --to or --self is required.' },
|
|
59
59
|
{ kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal'], required: false, default: 'normal', constraint: 'Delivery urgency. Every accepted tier may revive a dormant target, because the request must be answered; deferred is rejected for that reason.' },
|
|
60
|
-
{ kind: 'flag', name: 'output-schema', type: 'string', required: true, constraint: `JSON Schema the result must satisfy. ${OUTPUT_SCHEMA_TRANSPORT} The target answers
|
|
60
|
+
{ kind: 'flag', name: 'output-schema', type: 'string', required: true, constraint: `JSON Schema the result must satisfy. ${OUTPUT_SCHEMA_TRANSPORT} The target answers with \`crtr push result\` (or declines with \`crtr push result --decline "<reason>" --code <token>\`), then keeps working. ${OUTPUT_SCHEMA_CONSTRAINTS}` },
|
|
61
61
|
{ kind: 'flag', name: 'situational-context', type: 'string', required: false, constraint: 'Non-empty hidden ambient context delivered beside the message, or alone.' },
|
|
62
62
|
{ kind: 'flag', name: 'reopen', type: 'bool', required: false, hidden: true, constraint: 'Clear a finalized target latch before delivery or fresh revive.' },
|
|
63
63
|
];
|
|
@@ -129,7 +129,7 @@ const nodeMessageRequest = defineLeaf({
|
|
|
129
129
|
params: messageRequestParams(),
|
|
130
130
|
output: messageOutput(),
|
|
131
131
|
outputKind: 'object',
|
|
132
|
-
effects: ['Installs the one-off
|
|
132
|
+
effects: ['Installs the one-off output schema before appending the request.'],
|
|
133
133
|
},
|
|
134
134
|
run: (input) => {
|
|
135
135
|
requireNonBlankDiscriminant(input, 'outputSchema');
|
|
@@ -144,7 +144,7 @@ export const nodeMessage = defineBranch({
|
|
|
144
144
|
help: {
|
|
145
145
|
name: 'node message',
|
|
146
146
|
summary: 'deliver one immediate inbox entry to an existing node',
|
|
147
|
-
model: 'Both children append one inbox entry to a node that already exists, targeted by id or --self, and may revive it if it is dormant. They differ in what the delivery obligates. An ordinary message obligates nothing: the target reads it and acts however it judges best. A request installs a one-off
|
|
147
|
+
model: 'Both children append one inbox entry to a node that already exists, targeted by id or --self, and may revive it if it is dormant. They differ in what the delivery obligates. An ordinary message obligates nothing: the target reads it and acts however it judges best. A request installs a one-off output schema and holds the target awake and owing until it answers; the answer is validated against the schema, written to the target’s result file, and pushed as an update report to the TARGET’S subscribers, so subscribe to it first if the answer has to reach you. Neither child creates that standing relationship — a durable delivery edge is `node subscription`, and work that needs a fresh agent is `node new`.',
|
|
148
148
|
},
|
|
149
149
|
children: [nodeMessageSend, nodeMessageRequest],
|
|
150
150
|
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// `crtr node outcome` — read a durable terminal outcome through crtrd only.
|
|
2
|
+
import { isSafeNodeId } from '../../api/index.js';
|
|
3
|
+
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
4
|
+
import { InputError } from '../../core/io.js';
|
|
5
|
+
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
6
|
+
function nodeId(input) {
|
|
7
|
+
const id = typeof input['node'] === 'string' ? input['node'].trim() : '';
|
|
8
|
+
if (!isSafeNodeId(id)) {
|
|
9
|
+
throw new InputError({
|
|
10
|
+
error: 'invalid_node',
|
|
11
|
+
message: `invalid node id: ${id === '' ? '(empty)' : id}`,
|
|
12
|
+
field: 'node',
|
|
13
|
+
next: 'Pass the exact node id returned when the node was created.',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
function renderedJson(value) {
|
|
19
|
+
return value === null || value === undefined ? 'none' : JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
const nodeOutcomeRead = defineLeaf({
|
|
22
|
+
name: 'read',
|
|
23
|
+
description: 'read or briefly await a node’s durable terminal outcome',
|
|
24
|
+
whenToUse: 'an external caller needs the durable result or failure reason for one node, optionally blocking for its terminal outcome without consuming or claiming it',
|
|
25
|
+
help: {
|
|
26
|
+
name: 'node outcome read',
|
|
27
|
+
summary: 'read a node’s durable outcome, optionally awaiting it for up to 25 seconds',
|
|
28
|
+
params: [
|
|
29
|
+
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id whose durable outcome to read.' },
|
|
30
|
+
{ kind: 'flag', name: 'wait', type: 'int', required: false, default: 0, constraint: 'Whole seconds to await settlement: 0–25. Omit or use 0 to return the current pending/settled state immediately.' },
|
|
31
|
+
],
|
|
32
|
+
output: [
|
|
33
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The requested node id.' },
|
|
34
|
+
{ name: 'state', type: 'string', required: true, constraint: '`pending` while the node has no terminal outcome, otherwise `settled`.' },
|
|
35
|
+
{ name: 'outcome', type: 'object', required: true, constraint: 'The durable result/failure record when settled — kind/reason, revision, settled time, report basename/path, structured result, failure detail, and `declined` ({ reason, code, retryable } for a failure with reason `declined`, else null) — otherwise null.' },
|
|
36
|
+
{ name: 'node_status', type: 'string', required: true, constraint: 'The node status observed with the outcome.' },
|
|
37
|
+
{ name: 'deadline_at', type: 'string', required: true, constraint: 'The absolute spawn deadline, or null when unbounded.' },
|
|
38
|
+
],
|
|
39
|
+
outputKind: 'object',
|
|
40
|
+
effects: ['Read-only: calls the daemon’s replay-safe GET outcome route. With --wait, the daemon polls the durable row for no more than 25 seconds.'],
|
|
41
|
+
},
|
|
42
|
+
run: async (input) => {
|
|
43
|
+
const id = nodeId(input);
|
|
44
|
+
const wait = input['wait'];
|
|
45
|
+
if (typeof wait !== 'number' || !Number.isInteger(wait) || wait < 0 || wait > 25) {
|
|
46
|
+
throw new InputError({
|
|
47
|
+
error: 'invalid_wait',
|
|
48
|
+
message: '--wait must be an integer between 0 and 25 seconds',
|
|
49
|
+
field: 'wait',
|
|
50
|
+
received: wait,
|
|
51
|
+
next: 'Pass a whole number from 0 through 25, or omit --wait to poll immediately.',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { ...await cliClient().getNodeOutcome(id, { waitSeconds: wait }).catch(rethrowAsCliError) };
|
|
55
|
+
},
|
|
56
|
+
render: (result) => {
|
|
57
|
+
const outcome = result['outcome'];
|
|
58
|
+
if (outcome === null) {
|
|
59
|
+
return [
|
|
60
|
+
`<outcome state="pending" node_id="${result['node_id']}">`,
|
|
61
|
+
`- node status: ${result['node_status']}`,
|
|
62
|
+
`- deadline: ${result['deadline_at'] ?? 'none'}`,
|
|
63
|
+
'</outcome>',
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
`<outcome kind="${outcome['kind']}" reason="${outcome['reason']}" state="settled" node_id="${result['node_id']}" revision="${outcome['revision']}">`,
|
|
68
|
+
`- final report: ${outcome['final_report'] ?? 'none'}`,
|
|
69
|
+
`- report path: ${outcome['final_report_path'] ?? 'none'}`,
|
|
70
|
+
`- structured result: ${renderedJson(outcome['structured_result'])}`,
|
|
71
|
+
`- declined: ${renderedJson(outcome['declined'])}`,
|
|
72
|
+
`- failure detail: ${renderedJson(outcome['detail'])}`,
|
|
73
|
+
`- settled at: ${outcome['settled_at']}`,
|
|
74
|
+
`- node status: ${result['node_status']}`,
|
|
75
|
+
`- deadline: ${result['deadline_at'] ?? 'none'}`,
|
|
76
|
+
'</outcome>',
|
|
77
|
+
].join('\n');
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
export const nodeOutcomeDelivery = defineLeaf({
|
|
81
|
+
name: 'delivery',
|
|
82
|
+
description: 'inspect a node outcome-delivery registration',
|
|
83
|
+
whenToUse: 'you need the durable state of the local action target registered to receive a node’s outcome',
|
|
84
|
+
help: {
|
|
85
|
+
name: 'node outcome delivery',
|
|
86
|
+
summary: 'inspect the registered outcome-delivery target for one node',
|
|
87
|
+
params: [
|
|
88
|
+
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id whose outcome-delivery registration to inspect.' },
|
|
89
|
+
],
|
|
90
|
+
output: [
|
|
91
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The node whose outcome is delivered.' },
|
|
92
|
+
{ name: 'state', type: 'string', required: true, constraint: '`armed`, `pending`, `running`, `accepted`, or `permanent_failed`.' },
|
|
93
|
+
{ name: 'action', type: 'string', required: true, constraint: 'The resolved configured action name.' },
|
|
94
|
+
{ name: 'attempt', type: 'number', required: true, constraint: 'How many delivery attempts have been claimed.' },
|
|
95
|
+
{ name: 'next_attempt_at', type: 'string', required: true, constraint: 'ISO-8601 time of the next retry, or null before a retry is scheduled (including while armed).' },
|
|
96
|
+
{ name: 'accepted_at', type: 'string', required: true, constraint: 'When the target accepted the document, or null.' },
|
|
97
|
+
{ name: 'permanent_failed_at', type: 'string', required: true, constraint: 'When the target permanently rejected the document, or null.' },
|
|
98
|
+
{ name: 'last_failure', type: 'object', required: true, constraint: 'The most recent retryable or permanent delivery failure, or null.' },
|
|
99
|
+
{ name: 'created_at', type: 'string', required: true, constraint: 'When this delivery registration was created.' },
|
|
100
|
+
{ name: 'updated_at', type: 'string', required: true, constraint: 'When this delivery registration was last updated.' },
|
|
101
|
+
],
|
|
102
|
+
outputKind: 'object',
|
|
103
|
+
effects: ['Read-only: calls the daemon outcome-delivery inspection route.'],
|
|
104
|
+
},
|
|
105
|
+
run: async (input) => {
|
|
106
|
+
const id = nodeId(input);
|
|
107
|
+
return { ...await cliClient().getOutcomeDelivery(id).catch(rethrowAsCliError) };
|
|
108
|
+
},
|
|
109
|
+
render: (result) => {
|
|
110
|
+
return [
|
|
111
|
+
`<outcome-delivery state="${result['state']}" node_id="${result['node_id']}">`,
|
|
112
|
+
`- attempt: ${result['attempt']}`,
|
|
113
|
+
`- next retry: ${result['next_attempt_at'] ?? 'none'}`,
|
|
114
|
+
`- last failure: ${renderedJson(result['last_failure'])}`,
|
|
115
|
+
`- action: ${result['action']}`,
|
|
116
|
+
`- accepted at: ${result['accepted_at'] ?? 'none'}`,
|
|
117
|
+
`- permanently failed at: ${result['permanent_failed_at'] ?? 'none'}`,
|
|
118
|
+
`- registered at: ${result['created_at']}`,
|
|
119
|
+
`- updated at: ${result['updated_at']}`,
|
|
120
|
+
'</outcome-delivery>',
|
|
121
|
+
].join('\n');
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
export const nodeOutcome = defineBranch({
|
|
125
|
+
name: 'outcome',
|
|
126
|
+
description: 'read a node’s terminal outcome or its delivery state',
|
|
127
|
+
whenToUse: 'you need the durable terminal result/failure of one node, or the state of its registered outcome-delivery target',
|
|
128
|
+
help: {
|
|
129
|
+
name: 'node outcome',
|
|
130
|
+
summary: 'read one node’s durable terminal outcome or its delivery registration',
|
|
131
|
+
model: 'Outcome reads never consume the terminal record. Read returns the durable result or failure and can wait briefly for settlement; delivery inspects the separate action-registration attempt state.',
|
|
132
|
+
},
|
|
133
|
+
children: [nodeOutcomeRead, nodeOutcomeDelivery],
|
|
134
|
+
});
|
package/dist/commands/node.js
CHANGED
|
@@ -9,6 +9,7 @@ import { nodeSubscription } from './node/subscription.js';
|
|
|
9
9
|
import { nodeBash } from './node/bash.js';
|
|
10
10
|
import { pyramidSlash } from './node/pyramid.js';
|
|
11
11
|
import { nodeWorktreeBranch } from './node-worktree.js';
|
|
12
|
+
import { nodeOutcome } from './node/outcome.js';
|
|
12
13
|
export function registerNode() {
|
|
13
14
|
return defineBranch({
|
|
14
15
|
name: 'node',
|
|
@@ -23,6 +24,6 @@ export function registerNode() {
|
|
|
23
24
|
summary: 'create, inspect, message, and change canvas nodes',
|
|
24
25
|
model: 'A node is one agent on the canvas. Immediate actions, durable waits, subscriptions, lifecycle changes, and scheduled crons are separate intent families; choose the family by when the effect happens and what persistent state it changes.',
|
|
25
26
|
},
|
|
26
|
-
children: [nodeNew, nodeFork, nodeMessage, nodeWaitBranch, nodeSubscription, nodeInspect, nodeLifecycle, nodeWorktreeBranch, nodeConfig, nodePromote, nodeYield, nodeBash],
|
|
27
|
+
children: [nodeNew, nodeFork, nodeMessage, nodeWaitBranch, nodeSubscription, nodeOutcome, nodeInspect, nodeLifecycle, nodeWorktreeBranch, nodeConfig, nodePromote, nodeYield, nodeBash],
|
|
27
28
|
});
|
|
28
29
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
|
-
import { resolveProfileOperand
|
|
2
|
+
import { resolveProfileOperand } from '../../core/profiles/manifest.js';
|
|
3
|
+
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
3
4
|
const profileParam = {
|
|
4
5
|
kind: 'positional',
|
|
5
6
|
name: 'profile',
|
|
@@ -9,7 +10,7 @@ const profileParam = {
|
|
|
9
10
|
export const pauseLeaf = defineLeaf({
|
|
10
11
|
name: 'pause',
|
|
11
12
|
description: 'make a profile inert',
|
|
12
|
-
whenToUse: '
|
|
13
|
+
whenToUse: 'all work under a profile must stop now while in-flight bash commands continue under their detached supervisors.',
|
|
13
14
|
help: {
|
|
14
15
|
name: 'profile pause',
|
|
15
16
|
summary: 'pause a profile so it creates and revives no nodes',
|
|
@@ -19,15 +20,16 @@ export const pauseLeaf = defineLeaf({
|
|
|
19
20
|
{ name: 'paused_at', type: 'string', required: true, constraint: 'ISO 8601 timestamp when the profile became inert.' },
|
|
20
21
|
],
|
|
21
22
|
outputKind: 'object',
|
|
22
|
-
effects: ['Writes `paused_at
|
|
23
|
+
effects: ['Writes `paused_at`, SIGTERMs every live broker under the profile so its foreground bash hands off to its detached supervisor, and holds cron, deadline, revive, and inbox delivery until resume.'],
|
|
23
24
|
},
|
|
24
25
|
run: async (input) => {
|
|
25
26
|
const { profileId } = resolveProfileOperand(input['profile']);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
try {
|
|
28
|
+
return { ...await cliClient().pauseProfile(profileId) };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
rethrowAsCliError(error);
|
|
32
|
+
}
|
|
31
33
|
},
|
|
32
34
|
});
|
|
33
35
|
export const resumeLeaf = defineLeaf({
|
|
@@ -43,14 +45,15 @@ export const resumeLeaf = defineLeaf({
|
|
|
43
45
|
{ name: 'paused_at', type: 'null', required: true, constraint: 'Always null after resume; null means active.' },
|
|
44
46
|
],
|
|
45
47
|
outputKind: 'object',
|
|
46
|
-
effects: ['Clears `paused_at
|
|
48
|
+
effects: ['Clears `paused_at`, credits active node deadlines for the paused interval, and lets retained inbox mail, due crons, and interrupted turns resume.'],
|
|
47
49
|
},
|
|
48
50
|
run: async (input) => {
|
|
49
51
|
const { profileId } = resolveProfileOperand(input['profile']);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
try {
|
|
53
|
+
return { ...await cliClient().resumeProfile(profileId) };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
rethrowAsCliError(error);
|
|
57
|
+
}
|
|
55
58
|
},
|
|
56
59
|
});
|
package/dist/commands/push.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// crtr push update [body] — report progress to subscribers
|
|
4
4
|
// crtr push final [body] — finish: write result, mark node done, close window
|
|
5
|
+
// crtr push result [json] — answer a pending structured-output request
|
|
5
6
|
//
|
|
6
7
|
// "push" is THE verb a node uses to talk to its managers. `update` reports
|
|
7
8
|
// progress; its --tier sets delivery urgency. The caller's node is resolved from
|
|
@@ -39,6 +40,106 @@ const PUSH_WHEN_TO_USE = {
|
|
|
39
40
|
update: 'you have progress worth surfacing to your managers while work remains in flight. Use normal delivery unless it can wait for their next real wake (--tier deferred) or needs immediate attention (--tier urgent); use push final only when the work is actually done.',
|
|
40
41
|
final: 'the work this node was spawned to do is complete and you are ready to hand back the canonical result — this writes that result, marks the node done, and closes its window, so it is the LAST thing you do here, not a progress note. Use push update while work is still in flight, and do not reach for final on a node working directly with the user: it has no manager to report up to and would close mid-conversation (the guard blocks it unless you pass --force after the user confirms).',
|
|
41
42
|
};
|
|
43
|
+
/** `crtr push result` — submit the JSON answer to a pending structured-output
|
|
44
|
+
* request. A pure API client like its siblings: JSON is parsed client-side
|
|
45
|
+
* only to fail fast with a clean error; validation against the schema, the
|
|
46
|
+
* result write, the report push, and the schema clear all happen server-side
|
|
47
|
+
* in `POST /nodes/{id}/result`. */
|
|
48
|
+
function makeResultLeaf() {
|
|
49
|
+
return defineLeaf({
|
|
50
|
+
name: 'result',
|
|
51
|
+
description: 'answer a pending structured-output request with the required JSON result, or decline it',
|
|
52
|
+
whenToUse: 'your system prompt names a pending structured-output request you must answer. The value is validated against the required schema server-side; a mismatch rejects with field errors and records nothing, so fix and rerun. When the schema cannot be honestly satisfied (blocked, unavailable, bad input), pass `--decline "<reason>" --code <token>` (plus `--retryable` when the same request could succeed later) instead of inventing a value; your requester names the code tokens it accepts. For a terminal request either path IS how the node finishes — never call `crtr push final` alongside it.',
|
|
53
|
+
help: {
|
|
54
|
+
name: 'push result',
|
|
55
|
+
summary: 'answer a pending structured-output request',
|
|
56
|
+
params: [
|
|
57
|
+
{ kind: 'stdin', name: 'value', required: false, constraint: "The JSON result. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Required unless `--decline` is passed." },
|
|
58
|
+
{ kind: 'flag', name: 'decline', type: 'string', required: false, constraint: 'Decline instead of answering: one plain sentence on why the schema cannot be honestly satisfied (blocked, unavailable, bad input). Requires `--code`. Records a failure outcome with reason `declined` carrying the reason, code, and retryable flag, and clears the request; for a terminal request this finishes the node.' },
|
|
59
|
+
{ kind: 'flag', name: 'code', type: 'string', required: false, constraint: 'With `--decline`: one whitespace-free token (≤ 64 chars) naming the decline class. crouter carries it verbatim; the requester validates it against the tokens it documented for you.' },
|
|
60
|
+
{ kind: 'flag', name: 'retryable', type: 'bool', required: false, default: false, constraint: 'With `--decline`: the same request could succeed if issued again later (a transient block). Omit when the decline is final for this input.' },
|
|
61
|
+
],
|
|
62
|
+
output: [
|
|
63
|
+
{ name: 'mode', type: 'string', required: true, constraint: 'Which request this answered: `terminal` finished the node; `oneoff` left it working.' },
|
|
64
|
+
{ name: 'result_path', type: 'string', required: true, constraint: 'Absolute path of the recorded artifact: the result JSON, or the declined marker when `--decline` was passed.' },
|
|
65
|
+
{ name: 'declined', type: 'object', required: false, constraint: 'Present only when the request was declined: { reason, code, retryable } as recorded.' },
|
|
66
|
+
{ name: 'report_path', type: 'string', required: true, constraint: 'Absolute path of the report pushed alongside the result.' },
|
|
67
|
+
{ name: 'notified', type: 'string[]', required: true, constraint: 'Subscriber node ids that received an inbox pointer.' },
|
|
68
|
+
{ name: 'transitioned', type: 'object', required: false, constraint: 'Present only for a terminal request that flipped the node — { from, to } lifecycle-status transition applied server-side.' },
|
|
69
|
+
],
|
|
70
|
+
outputKind: 'object',
|
|
71
|
+
effects: [
|
|
72
|
+
'Validates the value against the pending schema server-side; a mismatch records nothing and the request stays pending.',
|
|
73
|
+
'Writes the accepted result to nodes/<nodeId>/context/result.json (or the decline to context/declined.json) and clears the pending request.',
|
|
74
|
+
'A terminal decline records a `failure` outcome with reason `declined` whose `declined` field carries { reason, code, retryable }; the requester sees no value.',
|
|
75
|
+
'Pushes the mode-matched report: final for a terminal request (the node finishes — same effects as `push final`), update for a one-off request (the node keeps working).',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
run: async (input) => {
|
|
79
|
+
const nodeId = requireCallerNode();
|
|
80
|
+
const decline = typeof input['decline'] === 'string' ? input['decline'].trim() : '';
|
|
81
|
+
const code = typeof input['code'] === 'string' ? input['code'].trim() : '';
|
|
82
|
+
let request;
|
|
83
|
+
if (decline !== '') {
|
|
84
|
+
if (code === '') {
|
|
85
|
+
throw new InputError({ error: 'missing_decline_code', message: 'a decline needs a code', field: 'code', next: 'Pass `--code <token>` — the whitespace-free token your requester documented for this decline class.' });
|
|
86
|
+
}
|
|
87
|
+
request = { decline, code, retryable: input['retryable'] === true };
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (code !== '' || input['retryable'] === true) {
|
|
91
|
+
throw new InputError({ error: 'decline_flags_without_decline', message: '--code and --retryable only apply with --decline', next: 'Pass `--decline "<reason>"` alongside them, or drop them and submit the JSON result.' });
|
|
92
|
+
}
|
|
93
|
+
let raw = typeof input['value'] === 'string' ? input['value'].trim() : '';
|
|
94
|
+
if (raw === '')
|
|
95
|
+
raw = (await readStdinRaw()).trim();
|
|
96
|
+
if (raw === '') {
|
|
97
|
+
throw new InputError({ error: 'missing_value', message: 'no result value', field: 'value', next: 'Pass the JSON result as an argument or on stdin, or `--decline "<reason>" --code <token>` when the schema cannot be honestly satisfied.' });
|
|
98
|
+
}
|
|
99
|
+
let value;
|
|
100
|
+
try {
|
|
101
|
+
value = JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
throw new InputError({
|
|
105
|
+
error: 'bad_result_json',
|
|
106
|
+
message: `the result is not valid JSON: ${err.message}`,
|
|
107
|
+
field: 'value',
|
|
108
|
+
next: 'Pass one valid JSON value matching the requested schema.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
request = { value };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = await cliClient().submitResult(nodeId, request);
|
|
115
|
+
if (result.mode === 'terminal')
|
|
116
|
+
markStopSignal(nodeId, 'pushedFinal');
|
|
117
|
+
return {
|
|
118
|
+
mode: result.mode,
|
|
119
|
+
result_path: result.result_path,
|
|
120
|
+
report_path: result.report_path,
|
|
121
|
+
notified: result.notified,
|
|
122
|
+
...(result.declined === undefined ? {} : { declined: result.declined }),
|
|
123
|
+
...(result.transitioned !== undefined ? { transitioned: result.transitioned } : {}),
|
|
124
|
+
...(result.worktree_auto_dropped === true
|
|
125
|
+
? { worktree_auto_dropped: true, worktree_auto_dropped_path: result.worktree_auto_dropped_path }
|
|
126
|
+
: {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
rethrowAsCliError(err);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
render: (r) => {
|
|
134
|
+
const declined = r['declined'];
|
|
135
|
+
const line = r['mode'] === 'terminal'
|
|
136
|
+
? `${declined ? 'Decline recorded' : 'Result recorded'} — node finished; its engine shuts down on next stop. Nothing more to do here: STOP your turn immediately. Reply with just "Done." and nothing else.`
|
|
137
|
+
: `${declined ? 'One-off request declined' : 'One-off result recorded'} and pushed — the structured-output gate is cleared; continue working normally.`;
|
|
138
|
+
const declinedLine = declined === undefined ? '' : `\n- code: ${declined.code} (${declined.retryable ? 'retryable' : 'not retryable'})`;
|
|
139
|
+
return `${line}\n\n- ${declined ? 'declined' : 'result'}: ${r['result_path']}${declinedLine}\n- report: ${r['report_path']}`;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
42
143
|
function makePushLeaf(kind) {
|
|
43
144
|
return defineLeaf({
|
|
44
145
|
name: kind,
|
|
@@ -159,6 +260,6 @@ export function registerPush() {
|
|
|
159
260
|
summary: 'push a report to your subscribers',
|
|
160
261
|
model: "A push writes a markdown report to the node's reports/ history and fans a lightweight pointer to every subscriber's inbox. Delivery inlines reports under 1,000 characters with their absolute path; larger reports remain pointers to dereference. Nothing is pushed automatically — the feed contains only what a node pushes explicitly, so push whenever you want a manager to see something. Pipe markdown via a single-quoted heredoc so it reaches the report literally. `push final` is how ANY node finishes: write the canonical result, mark done, close the window.",
|
|
161
262
|
},
|
|
162
|
-
children: [makePushLeaf('update'), makePushLeaf('final')],
|
|
263
|
+
children: [makePushLeaf('update'), makePushLeaf('final'), makeResultLeaf()],
|
|
163
264
|
});
|
|
164
265
|
}
|
|
@@ -14,7 +14,10 @@ test('resolveBundledPiCliPath uses crouter bundled pi instead of a standalone pi
|
|
|
14
14
|
try {
|
|
15
15
|
const cliPath = resolveBundledPiCliPath();
|
|
16
16
|
assert.equal(existsSync(cliPath), true);
|
|
17
|
-
|
|
17
|
+
// The path INSIDE the package is pi's own layout (0.84.4 moved it to
|
|
18
|
+
// dist/bundle/cli.js) and is read from its package.json `bin.pi` — assert
|
|
19
|
+
// only that we resolved into the bundled package.
|
|
20
|
+
assert.match(cliPath, /@earendil-works[\/]pi-coding-agent[\/].*cli\.js$/);
|
|
18
21
|
}
|
|
19
22
|
finally {
|
|
20
23
|
if (prevPath === undefined)
|
|
@@ -69,8 +69,14 @@ export class BrokerLimitsPanel {
|
|
|
69
69
|
this.move(1);
|
|
70
70
|
else if (matches("crtr.setup.list-up", data))
|
|
71
71
|
this.move(-1);
|
|
72
|
+
// DEL (\x7f) is what terminals send for backspace and its code point sits
|
|
73
|
+
// above the control range, so it has to be excluded here or editing an
|
|
74
|
+
// existing value is refused as a non-digit keystroke.
|
|
72
75
|
else if (data.length > 0 &&
|
|
73
|
-
[...data].every((char) =>
|
|
76
|
+
[...data].every((char) => {
|
|
77
|
+
const code = char.codePointAt(0);
|
|
78
|
+
return code >= 32 && code !== 127;
|
|
79
|
+
}))
|
|
74
80
|
this.notice = "Broker limits accept digits only.";
|
|
75
81
|
else {
|
|
76
82
|
this.selected().handleInput(data);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { envProfileId } from '../../../shared/env.js';
|
|
2
|
-
import { ModelSelectorComponent,
|
|
2
|
+
import { ModelSelectorComponent, ThinkingSelectorComponent, } from '@earendil-works/pi-coding-agent';
|
|
3
3
|
import { Input, truncateToWidth } from '@earendil-works/pi-tui';
|
|
4
4
|
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import { ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, isManagedProvider, readSubscriptionPool, } from '../../../core/subscription-state.js';
|
|
@@ -744,7 +744,7 @@ export class ModelsPanel {
|
|
|
744
744
|
this.notify();
|
|
745
745
|
}
|
|
746
746
|
showModelSelector(scoped, current, onPick) {
|
|
747
|
-
this.modelSelector = new ModelSelectorComponent(this.tui, current,
|
|
747
|
+
this.modelSelector = new ModelSelectorComponent(this.tui, current, this.runtime, scoped.map((model) => ({ model })), onPick, () => this.closePicker());
|
|
748
748
|
this.modelSelector.focused = true;
|
|
749
749
|
this.mode = "model-picker";
|
|
750
750
|
this.notify();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ModelRegistry, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { TUI, type Component, type Focusable } from '@earendil-works/pi-tui';
|
|
3
3
|
import { type BindingId, type BindingResolution } from '../../core/keybindings/index.js';
|
|
4
|
-
export type DestinationId = "providers" | "models" | "keybindings" | "display" | "profiles" | "panes" | "broker-limits" | "updates";
|
|
4
|
+
export type DestinationId = "providers" | "models" | "provider-options" | "keybindings" | "display" | "profiles" | "panes" | "broker-limits" | "updates";
|
|
5
5
|
export declare class SettingsShell implements Component, Focusable {
|
|
6
6
|
private readonly tui;
|
|
7
7
|
private readonly bindings;
|