@north-light/crouter 0.3.282 → 0.3.283

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.
Files changed (160) hide show
  1. package/dist/api/client.d.ts +19 -3
  2. package/dist/api/client.js +34 -0
  3. package/dist/api/dto/broker-ops.d.ts +11 -0
  4. package/dist/api/dto/broker.d.ts +3 -8
  5. package/dist/api/dto/common.d.ts +1 -1
  6. package/dist/api/dto/messages.d.ts +2 -2
  7. package/dist/api/dto/node-outcomes.d.ts +80 -0
  8. package/dist/api/dto/node-outcomes.js +2 -0
  9. package/dist/api/dto/nodes.d.ts +9 -0
  10. package/dist/api/dto/profiles.d.ts +5 -0
  11. package/dist/api/dto/reports.d.ts +45 -0
  12. package/dist/api/index.d.ts +1 -0
  13. package/dist/api/index.js +1 -0
  14. package/dist/api/routes.d.ts +7 -0
  15. package/dist/api/routes.js +7 -0
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +24 -3
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +31 -4
  18. package/dist/clients/attach/overlays/graph.d.ts +5 -0
  19. package/dist/clients/attach/overlays/graph.js +65 -47
  20. package/dist/clients/attach/overlays/pickers.js +8 -9
  21. package/dist/clients/attach/render/card-presentation.js +1 -1
  22. package/dist/clients/attach/viewer.js +1286 -1260
  23. package/dist/commands/api-client.js +22 -23
  24. package/dist/commands/node/create.js +38 -3
  25. package/dist/commands/node/inspect.js +3 -7
  26. package/dist/commands/node/message.js +3 -3
  27. package/dist/commands/node/outcome.d.ts +2 -0
  28. package/dist/commands/node/outcome.js +134 -0
  29. package/dist/commands/node.js +2 -1
  30. package/dist/commands/profile/pause.js +17 -14
  31. package/dist/commands/push.js +102 -1
  32. package/dist/commands/sys/__tests__/setup-core.test.js +4 -1
  33. package/dist/commands/sys/panels/broker-limits-panel.js +7 -1
  34. package/dist/commands/sys/panels/models-panel.js +2 -2
  35. package/dist/commands/sys/settings-shell.d.ts +1 -1
  36. package/dist/commands/sys/settings-shell.js +6 -0
  37. package/dist/commands/sys/settings.js +3 -3
  38. package/dist/core/__tests__/fixtures/fake-engine.d.ts +2 -0
  39. package/dist/core/__tests__/fixtures/fake-engine.js +42 -4
  40. package/dist/core/__tests__/helpers/harness.d.ts +1 -0
  41. package/dist/core/__tests__/helpers/harness.js +9 -0
  42. package/dist/core/__tests__/integration/profile-pause.test.d.ts +1 -0
  43. package/dist/core/__tests__/integration/profile-pause.test.js +115 -0
  44. package/dist/core/__tests__/node-outcome.test.d.ts +1 -0
  45. package/dist/core/__tests__/node-outcome.test.js +184 -0
  46. package/dist/core/__tests__/revive-parked-fresh.test.js +13 -20
  47. package/dist/core/__tests__/seam/broker-provider-retry.test.js +6 -2
  48. package/dist/core/__tests__/seam/dormancy-release.test.js +3 -3
  49. package/dist/core/__tests__/seam/node-outcome-delivery.test.d.ts +1 -0
  50. package/dist/core/__tests__/seam/node-outcome-delivery.test.js +153 -0
  51. package/dist/core/bash-jobs.d.ts +6 -0
  52. package/dist/core/bash-jobs.js +22 -0
  53. package/dist/core/canvas/canvas.d.ts +41 -3
  54. package/dist/core/canvas/canvas.js +153 -39
  55. package/dist/core/canvas/crons.js +2 -1
  56. package/dist/core/canvas/index.d.ts +1 -0
  57. package/dist/core/canvas/index.js +1 -0
  58. package/dist/core/canvas/migrations.js +47 -0
  59. package/dist/core/canvas/node-outcome-deliveries.d.ts +80 -0
  60. package/dist/core/canvas/node-outcome-deliveries.js +163 -0
  61. package/dist/core/canvas/pid.d.ts +7 -0
  62. package/dist/core/canvas/pid.js +17 -0
  63. package/dist/core/canvas/render-source.js +2 -2
  64. package/dist/core/canvas/types.d.ts +42 -1
  65. package/dist/core/canvas/types.js +2 -1
  66. package/dist/core/config.d.ts +4 -1
  67. package/dist/core/config.js +25 -1
  68. package/dist/core/feed/feed.d.ts +8 -0
  69. package/dist/core/feed/feed.js +25 -4
  70. package/dist/core/feed/inbox.d.ts +5 -0
  71. package/dist/core/feed/inbox.js +13 -0
  72. package/dist/core/keybindings/catalog.d.ts +1 -1
  73. package/dist/core/keybindings/catalog.js +3 -0
  74. package/dist/core/profiles/manifest.d.ts +3 -1
  75. package/dist/core/profiles/manifest.js +6 -1
  76. package/dist/core/runtime/broker/daemon-ops.d.ts +3 -1
  77. package/dist/core/runtime/broker/daemon-ops.js +6 -0
  78. package/dist/core/runtime/broker/engine-drive.d.ts +2 -3
  79. package/dist/core/runtime/broker/engine-drive.js +74 -27
  80. package/dist/core/runtime/broker/frame-dispatch.js +1 -0
  81. package/dist/core/runtime/broker/read-ops.js +14 -0
  82. package/dist/core/runtime/broker/rebind.js +0 -7
  83. package/dist/core/runtime/broker-protocol.d.ts +1 -6
  84. package/dist/core/runtime/fleet.d.ts +7 -0
  85. package/dist/core/runtime/headless-pi.d.ts +13 -0
  86. package/dist/core/runtime/headless-pi.js +10 -2
  87. package/dist/core/runtime/lifecycle.d.ts +6 -1
  88. package/dist/core/runtime/lifecycle.js +50 -1
  89. package/dist/core/runtime/nodes.d.ts +5 -1
  90. package/dist/core/runtime/nodes.js +2 -1
  91. package/dist/core/runtime/outcome-document.d.ts +70 -0
  92. package/dist/core/runtime/outcome-document.js +97 -0
  93. package/dist/core/runtime/revive.js +16 -21
  94. package/dist/core/runtime/spawn.d.ts +5 -1
  95. package/dist/core/runtime/spawn.js +18 -7
  96. package/dist/core/runtime/stamp/channel.d.ts +3 -5
  97. package/dist/core/runtime/stamp/channel.js +3 -10
  98. package/dist/core/runtime/stop-guard.js +2 -2
  99. package/dist/core/runtime/structured-output.d.ts +24 -3
  100. package/dist/core/runtime/structured-output.js +42 -3
  101. package/dist/core/runtime/tmux-bindings.js +4 -0
  102. package/dist/core/runtime/warm-pool.d.ts +5 -2
  103. package/dist/core/runtime/warm-pool.js +5 -5
  104. package/dist/core/subscription-state.js +7 -5
  105. package/dist/core/substrate/render.js +10 -10
  106. package/dist/core/user-settings.d.ts +18 -1
  107. package/dist/core/user-settings.js +37 -0
  108. package/dist/daemon/__tests__/node-deadline-lane.test.d.ts +1 -0
  109. package/dist/daemon/__tests__/node-deadline-lane.test.js +76 -0
  110. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.d.ts +1 -0
  111. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +122 -0
  112. package/dist/daemon/__tests__/node-outcome-delivery.test.d.ts +1 -0
  113. package/dist/daemon/__tests__/node-outcome-delivery.test.js +86 -0
  114. package/dist/daemon/api/__tests__/broker-settle-park.test.js +86 -18
  115. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +10 -1
  116. package/dist/daemon/api/__tests__/seam/node-outcome-api.test.d.ts +1 -0
  117. package/dist/daemon/api/__tests__/seam/node-outcome-api.test.js +159 -0
  118. package/dist/daemon/api/__tests__/submit-result.test.d.ts +1 -0
  119. package/dist/daemon/api/__tests__/submit-result.test.js +214 -0
  120. package/dist/daemon/api/handlers/broker-ops.js +53 -24
  121. package/dist/daemon/api/handlers/canvas.js +2 -2
  122. package/dist/daemon/api/handlers/messages.js +12 -2
  123. package/dist/daemon/api/handlers/node-outcomes.d.ts +2 -0
  124. package/dist/daemon/api/handlers/node-outcomes.js +136 -0
  125. package/dist/daemon/api/handlers/nodes.js +67 -1
  126. package/dist/daemon/api/handlers/profiles.js +53 -1
  127. package/dist/daemon/api/handlers/reports.js +197 -33
  128. package/dist/daemon/api/map.d.ts +7 -2
  129. package/dist/daemon/api/map.js +78 -4
  130. package/dist/daemon/api/server.js +2 -0
  131. package/dist/daemon/crtrd.js +12 -0
  132. package/dist/daemon/fleet.d.ts +4 -8
  133. package/dist/daemon/fleet.js +48 -9
  134. package/dist/daemon/messaging/node-message.js +2 -1
  135. package/dist/daemon/node-outcome/deliver-outcome.d.ts +14 -0
  136. package/dist/daemon/node-outcome/deliver-outcome.js +155 -0
  137. package/dist/daemon/park-activity.d.ts +9 -0
  138. package/dist/daemon/park-activity.js +30 -0
  139. package/dist/daemon/park-pending.d.ts +10 -5
  140. package/dist/daemon/park-pending.js +18 -8
  141. package/dist/daemon/reconcilers/bash-deadline.js +6 -0
  142. package/dist/daemon/reconcilers/broker-supervision.d.ts +3 -7
  143. package/dist/daemon/reconcilers/broker-supervision.js +29 -33
  144. package/dist/daemon/reconcilers/node-deadline.d.ts +5 -0
  145. package/dist/daemon/reconcilers/node-deadline.js +82 -0
  146. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -1
  147. package/dist/daemon/reconcilers/node-lifecycle/tick.js +12 -3
  148. package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +10 -0
  149. package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +41 -0
  150. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +66 -66
  151. package/dist/pi-extensions/canvas-structured-output.d.ts +9 -5
  152. package/dist/pi-extensions/canvas-structured-output.js +23 -297
  153. package/dist/shared/env.d.ts +3 -0
  154. package/dist/shared/env.js +6 -0
  155. package/dist/shared/generated-context.d.ts +3 -9
  156. package/dist/shared/generated-context.js +8 -16
  157. package/dist/types.d.ts +12 -0
  158. package/dist/types.js +1 -0
  159. package/package.json +8 -8
  160. package/runtime.lock.json +227 -383
@@ -1,309 +1,35 @@
1
- // canvas-structured-output.ts — dynamic `submit` tool for schema-shaped node results.
1
+ // canvas-structured-output.ts — surfaces a pending structured-output request in
2
+ // the system prompt so the node answers it with `crtr push result`.
2
3
  //
3
- // Loaded into every canvas node. Inert unless nodes/<id>/output-schema.json exists
4
- // and is valid. If the request file is malformed, the tool is not registered — the
5
- // stop-guard will reprompt the node with an explicit error about the invalid file.
4
+ // Loaded into every canvas node. Inert unless nodes/<id>/output-schema.json
5
+ // exists and is valid; the request file is re-read every agent run (never
6
+ // cached), so a request installed or cleared between runs takes effect on the
7
+ // next run. If the file is malformed, nothing is injected — the stop guard owns
8
+ // the error surface and reprompts the node about the invalid file.
9
+ //
10
+ // Validation, the result write, the report push, and the schema clear all live
11
+ // server-side behind `POST /nodes/{id}/result` — this extension only tells the
12
+ // model that a request is pending and how to answer it.
6
13
  import { envNodeId } from '../shared/env.js';
7
- import { mkdirSync, writeFileSync } from 'node:fs';
8
- import { dirname } from 'node:path';
9
- import { validateToolArguments } from '@earendil-works/pi-ai';
10
- import { emitEvent } from '../core/events/emit.js';
11
- import { publishBrokerReport } from '../core/runtime/broker/daemon-ops.js';
12
- import { outputResultPath, readOutputRequest, removeOutputSchema, } from '../core/runtime/structured-output.js';
13
- function reportBody(resultPath, json) {
14
- return `Structured output submitted.\n\nResult path: ${resultPath}\n\n\`\`\`json\n${json}\n\`\`\`\n`;
15
- }
16
- function activateTool(pi) {
17
- try {
18
- const current = pi.getActiveTools?.() ?? [];
19
- if (!current.includes('submit'))
20
- pi.setActiveTools?.([...current, 'submit']);
21
- }
22
- catch {
23
- /* best-effort; registration refresh normally activates new extension tools */
24
- }
25
- }
26
- function retireTool(pi) {
27
- try {
28
- const current = pi.getActiveTools?.() ?? [];
29
- pi.setActiveTools?.(current.filter((name) => name !== 'submit'));
30
- }
31
- catch {
32
- /* best-effort; the execute handler is inert when no schema file exists */
33
- }
34
- }
35
- const STRICT_STRING_FORMATS = new Set([
36
- 'date-time', 'time', 'date', 'duration', 'email', 'hostname', 'uri', 'ipv4', 'ipv6', 'uuid',
37
- ]);
38
- // Provider strict modes accept a structural subset; omitted constraints stay visible in descriptions.
39
- function providerStrictSchema(schema) {
40
- const transform = (value) => {
41
- if (value === null || typeof value !== 'object' || Array.isArray(value))
42
- return value;
43
- const source = { ...value };
44
- const take = (key) => {
45
- const child = source[key];
46
- delete source[key];
47
- return child;
48
- };
49
- const transformed = {};
50
- const ref = take('$ref');
51
- if (ref !== undefined)
52
- return { $ref: ref };
53
- const defs = take('$defs');
54
- if (defs !== null && typeof defs === 'object' && !Array.isArray(defs)) {
55
- transformed['$defs'] = Object.fromEntries(Object.entries(defs).map(([name, definition]) => [name, transform(definition)]));
56
- }
57
- const type = take('type');
58
- const anyOf = take('anyOf');
59
- const oneOf = take('oneOf');
60
- const allOf = take('allOf');
61
- if (Array.isArray(anyOf))
62
- transformed['anyOf'] = anyOf.map(transform);
63
- else if (Array.isArray(oneOf))
64
- transformed['anyOf'] = oneOf.map(transform);
65
- else if (Array.isArray(allOf))
66
- transformed['allOf'] = allOf.map(transform);
67
- else if (type !== undefined)
68
- transformed['type'] = type;
69
- else
70
- return value;
71
- const description = take('description');
72
- if (description !== undefined)
73
- transformed['description'] = description;
74
- const title = take('title');
75
- if (title !== undefined)
76
- transformed['title'] = title;
77
- if (type === 'object') {
78
- const properties = take('properties');
79
- transformed['properties'] = properties !== null && typeof properties === 'object' && !Array.isArray(properties)
80
- ? Object.fromEntries(Object.entries(properties).map(([name, property]) => [name, transform(property)]))
81
- : {};
82
- take('additionalProperties');
83
- transformed['additionalProperties'] = false;
84
- const required = take('required');
85
- if (required !== undefined)
86
- transformed['required'] = required;
87
- }
88
- else if (type === 'string') {
89
- const format = take('format');
90
- if (typeof format === 'string' && STRICT_STRING_FORMATS.has(format))
91
- transformed['format'] = format;
92
- else if (format !== undefined)
93
- source['format'] = format;
94
- }
95
- else if (type === 'array') {
96
- const items = take('items');
97
- if (items !== undefined)
98
- transformed['items'] = transform(items);
99
- const minItems = take('minItems');
100
- if (minItems === 0 || minItems === 1)
101
- transformed['minItems'] = minItems;
102
- else if (minItems !== undefined)
103
- source['minItems'] = minItems;
104
- }
105
- if (Object.keys(source).length > 0) {
106
- const existing = typeof transformed['description'] === 'string' ? transformed['description'] : '';
107
- const constraints = `{${Object.entries(source).map(([key, child]) => `${key}: ${JSON.stringify(child)}`).join(', ')}}`;
108
- transformed['description'] = existing === '' ? constraints : `${existing}\n\n${constraints}`;
109
- }
110
- return transformed;
111
- };
112
- return transform(schema);
113
- }
114
- function strictToolSchema(schema) {
115
- const transform = (value) => {
116
- if (Array.isArray(value))
117
- return value.map(transform);
118
- if (value === null || typeof value !== 'object')
119
- return value;
120
- const source = value;
121
- const transformed = {};
122
- for (const [key, child] of Object.entries(source)) {
123
- if (key === 'properties' && child !== null && typeof child === 'object' && !Array.isArray(child)) {
124
- transformed[key] = Object.fromEntries(Object.entries(child).map(([name, property]) => [name, transform(property)]));
125
- }
126
- else {
127
- transformed[key] = transform(child);
128
- }
129
- }
130
- const properties = transformed['properties'];
131
- const isObjectSchema = transformed['type'] === 'object'
132
- || (properties !== null && typeof properties === 'object' && !Array.isArray(properties));
133
- if (isObjectSchema)
134
- transformed['additionalProperties'] = false;
135
- if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
136
- return transformed;
137
- const required = new Set(Array.isArray(source['required']) ? source['required'].filter((name) => typeof name === 'string') : []);
138
- transformed['properties'] = Object.fromEntries(Object.entries(properties).map(([name, property]) => [
139
- name,
140
- required.has(name) ? property : { anyOf: [property, { type: 'null' }] },
141
- ]));
142
- transformed['required'] = Object.keys(properties);
143
- return transformed;
144
- };
145
- return providerStrictSchema(transform(schema));
146
- }
147
- function validateResult(value, schema) {
148
- return validateToolArguments({ name: 'submit', description: '', parameters: schema }, { type: 'toolCall', id: 'structured-output', name: 'submit', arguments: value });
149
- }
150
- function omitStrictNulls(value, schema) {
151
- if (value === null || typeof value !== 'object' || Array.isArray(value) || schema === null || typeof schema !== 'object' || Array.isArray(schema))
152
- return value;
153
- const source = schema;
154
- const properties = source['properties'];
155
- if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
156
- return value;
157
- const requiredNames = source['required'];
158
- const required = new Set(Array.isArray(requiredNames) ? requiredNames.filter((name) => typeof name === 'string') : []);
159
- return Object.fromEntries(Object.entries(value).flatMap(([name, child]) => {
160
- if (child === null && !required.has(name) && Object.hasOwn(properties, name))
161
- return [];
162
- return [[name, omitStrictNulls(child, properties[name])]];
163
- }));
164
- }
165
- function toolDescription(request) {
166
- if (request.mode === 'terminal') {
167
- return 'Submit your final structured result. Calling this ends your run: the result is recorded and a final report is pushed automatically — do NOT call `crtr push final` yourself.';
168
- }
169
- return 'Submit the requested one-off structured result. Calling this records the result, pushes an update report automatically, frees you from the structured-output gate, and lets you continue working.';
14
+ import { readOutputRequest } from '../core/runtime/structured-output.js';
15
+ /** Render the prompt block for one valid request. Exported for the test suite. */
16
+ export function structuredOutputPromptBlock(request) {
17
+ const schema = JSON.stringify(request.schema, null, 2);
18
+ const mandate = request.mode === 'terminal'
19
+ ? 'This node owes a terminal structured result. When the work is complete, submit the result with `crtr push result` (pipe the JSON on stdin) — that records it and pushes your final report in one step, finishing this node. Do NOT call `crtr push final` yourself; it is blocked while this request is pending. If the schema cannot be honestly satisfied (blocked, unavailable, bad input), decline with `crtr push result --decline "<reason>" --code <token>` instead of inventing a value — that finishes the node with a declined outcome. You cannot stop or go dormant until the result is accepted or declined.'
20
+ : 'This node owes a one-off structured result. Submit it with `crtr push result` (pipe the JSON on stdin) — that records it and pushes an update report to your subscribers, then you continue working normally. If the schema cannot be honestly satisfied, decline with `crtr push result --decline "<reason>" --code <token>` instead of inventing a value. You cannot stop or go dormant until the result is accepted or declined.';
21
+ return `<structured-output-request>\n${mandate}\n\nThe submitted JSON is validated against this schema; a mismatch rejects with field errors and records nothing, so fix and rerun.\n\nRequired schema:\n\n\`\`\`json\n${schema}\n\`\`\`\n</structured-output-request>`;
170
22
  }
171
23
  export function registerCanvasStructuredOutput(pi) {
172
24
  const nodeId = envNodeId();
173
25
  if (nodeId === undefined || nodeId.trim() === '')
174
26
  return;
175
- let registeredKey = null;
176
- let active = false;
177
- const ensureSubmitTool = (activate) => {
27
+ pi.on('before_agent_start', (event) => {
178
28
  const state = readOutputRequest(nodeId);
179
- // If no request exists, retire the tool if it was active.
180
- if (state.state === 'absent') {
181
- if (active) {
182
- active = false;
183
- retireTool(pi);
184
- }
29
+ if (state.state !== 'valid')
185
30
  return;
186
- }
187
- // If the request is invalid, do not register or activate the tool.
188
- // The stop-guard will reprompt with an explicit error message.
189
- if (state.state === 'invalid') {
190
- if (active) {
191
- active = false;
192
- retireTool(pi);
193
- }
194
- return;
195
- }
196
- // The request is valid; proceed with tool registration and activation.
197
- const request = state.request;
198
- const key = JSON.stringify(request);
199
- if (registeredKey !== key) {
200
- pi.registerTool({
201
- name: 'submit',
202
- label: 'Submit',
203
- description: toolDescription(request),
204
- promptSnippet: request.mode === 'terminal'
205
- ? 'Submit the final structured result and end this node'
206
- : 'Submit the requested one-off structured result',
207
- promptGuidelines: request.mode === 'terminal'
208
- ? [
209
- 'Use submit as your final action when the required structured result is ready.',
210
- 'After calling submit, do not call crtr push final yourself and do not emit another assistant response.',
211
- ]
212
- : [
213
- 'Use submit to answer the pending one-off structured-output request before stopping or going dormant.',
214
- 'After one-off submit succeeds, continue with any remaining work normally.',
215
- ],
216
- // Strict provider tools require every property to be required and every
217
- // object to reject undeclared properties. Optional fields become nullable
218
- // in the provider-only schema and null is removed before persistence.
219
- parameters: strictToolSchema(request.schema),
220
- constrainedSampling: { type: 'json_schema', strict: 'prefer' },
221
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
222
- const current = readOutputRequest(nodeId);
223
- if (current.state === 'absent') {
224
- return {
225
- content: [{ type: 'text', text: 'No structured output is currently requested.' }],
226
- details: { submitted: false, reason: 'no_request' },
227
- };
228
- }
229
- if (current.state === 'invalid') {
230
- return {
231
- content: [{ type: 'text', text: `Structured-output request is invalid: ${current.error}` }],
232
- details: { submitted: false, reason: 'invalid_request', error: current.error },
233
- };
234
- }
235
- if (JSON.stringify(current.request) !== key) {
236
- registeredKey = null;
237
- ensureSubmitTool(true);
238
- return {
239
- content: [{ type: 'text', text: 'The structured-output request changed while you were working. Call submit again using the current schema.' }],
240
- details: { submitted: false, reason: 'schema_replaced' },
241
- };
242
- }
243
- const result = validateResult(omitStrictNulls(params, current.request.schema), current.request.schema);
244
- const resultPath = outputResultPath(nodeId);
245
- const json = JSON.stringify(result, null, 2);
246
- mkdirSync(dirname(resultPath), { recursive: true });
247
- writeFileSync(resultPath, `${json}\n`, 'utf8');
248
- const body = reportBody(resultPath, json);
249
- try {
250
- await publishBrokerReport(nodeId, {
251
- tier: current.request.mode === 'terminal' ? 'final' : 'update',
252
- body,
253
- });
254
- }
255
- catch (error) {
256
- // A rejected daemon operation leaves the schema active and does not
257
- // terminate Pi, so the submitted result remains retryable.
258
- emitEvent({
259
- event: 'broker.daemon.operation.failed',
260
- level: 'error',
261
- node_id: nodeId,
262
- error,
263
- fields: { operation: 'structured_output_report' },
264
- });
265
- throw error;
266
- }
267
- if (current.request.mode === 'terminal') {
268
- removeOutputSchema(nodeId);
269
- active = false;
270
- retireTool(pi);
271
- try {
272
- ctx?.abort?.();
273
- }
274
- catch { /* ignore */ }
275
- try {
276
- ctx?.shutdown?.();
277
- }
278
- catch { /* ignore */ }
279
- return {
280
- content: [{ type: 'text', text: `Structured output submitted and finalized: ${resultPath}` }],
281
- details: { submitted: true, mode: current.request.mode, resultPath, result },
282
- terminate: true,
283
- };
284
- }
285
- removeOutputSchema(nodeId);
286
- active = false;
287
- retireTool(pi);
288
- return {
289
- content: [{ type: 'text', text: `Structured output submitted: ${resultPath}` }],
290
- details: { submitted: true, mode: current.request.mode, resultPath, result },
291
- };
292
- },
293
- });
294
- registeredKey = key;
295
- }
296
- if (activate && !active) {
297
- active = true;
298
- activateTool(pi);
299
- }
300
- };
301
- ensureSubmitTool(false);
302
- pi.on('before_agent_start', () => {
303
- ensureSubmitTool(true);
304
- });
305
- pi.on('agent_start', () => {
306
- ensureSubmitTool(true);
31
+ // Appended at the END so the mandate lands after every other prompt layer.
32
+ return { systemPrompt: `${event.systemPrompt}\n\n${structuredOutputPromptBlock(state.request)}` };
307
33
  });
308
34
  }
309
35
  export default registerCanvasStructuredOutput;
@@ -82,3 +82,6 @@ export declare function envUnattendedParkMs(): number | undefined;
82
82
  /** Test-only park-summary grace-period override
83
83
  * (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
84
84
  export declare function envTestParkSummaryGraceMs(): number | undefined;
85
+ /** The bounded lifetime shared by the daemon's pending-park sweep and its
86
+ * in-broker isolated turn. */
87
+ export declare function parkSummaryGraceMs(): number;
@@ -195,3 +195,9 @@ export function envUnattendedParkMs() {
195
195
  export function envTestParkSummaryGraceMs() {
196
196
  return parsePositiveMsNoDefault(process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS']);
197
197
  }
198
+ const DEFAULT_PARK_SUMMARY_GRACE_MS = 5 * 60_000;
199
+ /** The bounded lifetime shared by the daemon's pending-park sweep and its
200
+ * in-broker isolated turn. */
201
+ export function parkSummaryGraceMs() {
202
+ return envTestParkSummaryGraceMs() ?? DEFAULT_PARK_SUMMARY_GRACE_MS;
203
+ }
@@ -8,15 +8,9 @@ export declare const CONTEXT_NUDGE_CUSTOM_TYPE = "crtr-context-nudge";
8
8
  export declare const REVIEW_BOUNDARY_CUSTOM_TYPE = "crtr-review-boundary";
9
9
  /** Generic completion mandate issued by the terminal-node stop guard. */
10
10
  export declare const STALL_REPROMPT: string;
11
- /** The daemon's parking mandate: the last turn of a conversation the unattended
12
- * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
13
- * governs both the node's durable inheritance and reader-facing output:
14
- * one update for subscribers and history, then a brief completion reply.
15
- *
16
- * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
17
- * guide (roadmap current, short and shrinking; context dir for in-progress
18
- * material; memory only for gated permanent lessons) but not a string: yield is
19
- * read by an agent choosing to refresh, this by an agent being told to conclude. */
11
+ /** The daemon's parking mandate for an isolated turn. It leaves current truth,
12
+ * supporting material when needed, lasting lessons, and one deferred update;
13
+ * the main conversation remains untouched and resumes without a fresh cycle. */
20
14
  export declare const PARK_SUMMARY_PROMPT: string;
21
15
  /** Static recovery prompts shared by the broker producer and display classifier. */
22
16
  export declare const AUTH_FAULT_RECOVERY_BODY = "Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.";
@@ -13,22 +13,14 @@ export const REVIEW_BOUNDARY_CUSTOM_TYPE = 'crtr-review-boundary';
13
13
  /** Generic completion mandate issued by the terminal-node stop guard. */
14
14
  export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
15
15
  "Pipe the result to `crtr push final` through a single-quoted heredoc if the work is done, or use `crtr human send` if you are blocked or need the user.";
16
- /** The daemon's parking mandate: the last turn of a conversation the unattended
17
- * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
18
- * governs both the node's durable inheritance and reader-facing output:
19
- * one update for subscribers and history, then a brief completion reply.
20
- *
21
- * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
22
- * guide (roadmap current, short and shrinking; context dir for in-progress
23
- * material; memory only for gated permanent lessons) but not a string: yield is
24
- * read by an agent choosing to refresh, this by an agent being told to conclude. */
25
- export const PARK_SUMMARY_PROMPT = 'This conversation has been idle with nothing left to wake it, so it is being concluded. This is your last turn. Use it to leave a trustworthy inheritance, not to restart or broaden the work. Do these six things now, then stop.\n\n'
16
+ /** The daemon's parking mandate for an isolated turn. It leaves current truth,
17
+ * supporting material when needed, lasting lessons, and one deferred update;
18
+ * the main conversation remains untouched and resumes without a fresh cycle. */
19
+ export const PARK_SUMMARY_PROMPT = 'This conversation has been idle with nothing left to wake it, so it is being concluded. This is your last turn. Use it to leave a trustworthy inheritance, not to restart or broaden the work. Do these four things now, then stop.\n\n'
26
20
  + '1. Establish current truth. Check only state that may have changed outside the transcript and matters to resuming—such as the working tree, a remote run, or an external decision. Do not start new work; perform only a quick check needed to avoid recording an unverified claim. If no mandate or work ever began, record that plainly in the inheritance and keep every artifact minimal.\n\n'
27
- + '2. Rewrite `$CRTR_CONTEXT_DIR/roadmap.md` for a fresh context window. This is the only handoff document a later fresh cycle receives in full. Preserve the current goal and exit criteria when they still apply, then state the present outcome; what remains or is blocked; decisions or questions still open; exact recovery handles for in-flight state; and the first safe move on return. Keep strategy and present state, not a transcript recap. Delete stale and completed steps instead of marking them done; the roadmap should stay short and shrink. Write a minimal one now if none exists.\n\n'
28
- + '3. Put supporting material in your context directory only when the roadmap would become bulky without it. Rewrite existing living documents rather than leave superseded versions. Name every supporting file the next cycle must read from the roadmap and say what it is for—the revive shows filenames but does not inject their contents. Task state, identifiers, and recovery detail belong here, not in memory.\n\n'
29
- + '4. Use memory only for a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr memory write -h`, find before writing, and choose the narrowest scope that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into memory.\n\n'
30
- + '5. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write for someone who has not seen this conversation: name the work in plain terms, say where it stands, and say what remains or is blocked. Put the current outcome in the first line; include only needed decisions and recovery handles. This concludes the conversation, not the mandate.\n\n'
31
- + '6. When finished, reply with exactly `done`.';
21
+ + '2. Put supporting material in your context directory only when it is needed to keep the inheritance concise. Rewrite existing living documents rather than leave superseded versions. Task state, identifiers, and recovery detail belong here, not in memory.\n\n'
22
+ + '3. Use memory only for a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr memory write -h`, find before writing, and choose the narrowest scope that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into memory.\n\n'
23
+ + '4. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write for someone who has not seen this conversation: name the work in plain terms, say where it stands, and say what remains or is blocked. Put the current outcome in the first line; include only needed decisions and recovery handles, then stop.';
32
24
  /** Static recovery prompts shared by the broker producer and display classifier. */
33
25
  export const AUTH_FAULT_RECOVERY_BODY = 'Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.';
34
26
  export const CONNECTION_FAULT_RECOVERY_BODY = 'The network connection is back online. Your previous turn stopped on a connection error (the network was down). Continue from where you left off and retry the work that failed.';
@@ -37,7 +29,7 @@ const REVIEW_APPROVAL_OPEN = '<crtr-review-approval>';
37
29
  const REVIEW_APPROVAL_CLOSE = '</crtr-review-approval>';
38
30
  const MODEL_FALLBACK_RECOVERY_OPEN = '<model-fallback-recovery>';
39
31
  const MODEL_FALLBACK_RECOVERY_CLOSE = '</model-fallback-recovery>';
40
- const STRUCTURED_OUTPUT_REPROMPT_PREFIX = 'You must call the `submit` tool with a result matching the required schema before you can stop. You cannot finish or go dormant any other way while this request is pending.\n\nRequired schema:\n\n```json\n';
32
+ const STRUCTURED_OUTPUT_REPROMPT_PREFIX = 'You must submit a result matching the required schema with `crtr push result` before you can stop, or decline it with `crtr push result --decline "<reason>" --code <token>` when the schema cannot be honestly satisfied. You cannot finish or go dormant any other way while this request is pending.\n\nRequired schema:\n\n```json\n';
41
33
  const STRUCTURED_OUTPUT_REPROMPT_SUFFIX = '\n```';
42
34
  /** Format the stop guard's dynamic structured-output mandate. */
43
35
  export function formatStructuredOutputReprompt(schema) {
package/dist/types.d.ts CHANGED
@@ -211,6 +211,14 @@ export interface ModelRoutingConfig {
211
211
  familyOrder?: string[];
212
212
  strengthFallback?: ModelStrength[];
213
213
  }
214
+ /** Per-provider behavior options, keyed by RUNTIME provider id (`openai-codex`,
215
+ * `anthropic`) — not the portable ladder key (`openai`). Merged per provider
216
+ * across scopes like the rest of `ScopeConfig`. */
217
+ export interface ProviderOptionsConfig {
218
+ /** Codex fast mode: ask the ChatGPT backend to serve the turn at its
219
+ * priority tier (~1.5x generation speed, ~2x credit consumption). */
220
+ fastMode?: boolean;
221
+ }
214
222
  /** Launch metadata for one kind (a top-level kind like `developer`, or a full
215
223
  * sub-kind string like `audit/security`) — the settings-file
216
224
  * replacement for per-kind persona frontmatter. `model`/`tools`/`extensions`
@@ -330,6 +338,10 @@ export interface ScopeConfig {
330
338
  modelRoutes?: Record<string, ModelRouteConfig>;
331
339
  /** Ordering overrides for declared model routes. */
332
340
  modelRouting?: ModelRoutingConfig;
341
+ /** Per-provider behavior options keyed by runtime provider id
342
+ * (`openai-codex`, `anthropic`). Merged per provider across scopes: a
343
+ * nearer scope overrides only the keys it names. */
344
+ providerOptions: Record<string, ProviderOptionsConfig>;
333
345
  /** The kind registry (spec §1.5): kind existence + launch knobs, keyed by
334
346
  * full kind string (top-level e.g. `developer`, or sub-kind e.g.
335
347
  * `audit/security`). Builtins ship a default registry via
package/dist/types.js CHANGED
@@ -103,6 +103,7 @@ export function defaultScopeConfig() {
103
103
  keybindings: {},
104
104
  tmux_passthrough: [],
105
105
  modelLadders: defaultModelLaddersConfig(),
106
+ providerOptions: {},
106
107
  kinds: defaultKindsConfig(),
107
108
  remoteCanvas: defaultRemoteCanvasConfig(),
108
109
  spawnEnv: { allow: [] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.282",
3
+ "version": "0.3.283",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -93,9 +93,9 @@
93
93
  "test:all": "npm test && npm run test:integration && npm run test:seam"
94
94
  },
95
95
  "overrides": {
96
- "@earendil-works/pi-ai": "0.84.1",
97
- "@earendil-works/pi-agent-core": "0.84.1",
98
- "@earendil-works/pi-tui": "0.84.1"
96
+ "@earendil-works/pi-ai": "0.84.4",
97
+ "@earendil-works/pi-agent-core": "0.84.4",
98
+ "@earendil-works/pi-tui": "0.84.4"
99
99
  },
100
100
  "repository": {
101
101
  "type": "git",
@@ -106,10 +106,10 @@
106
106
  },
107
107
  "license": "MIT",
108
108
  "dependencies": {
109
- "@earendil-works/pi-agent-core": "0.84.1",
110
- "@earendil-works/pi-ai": "0.84.1",
111
- "@earendil-works/pi-coding-agent": "0.84.1",
112
- "@earendil-works/pi-tui": "0.84.1",
109
+ "@earendil-works/pi-agent-core": "0.84.4",
110
+ "@earendil-works/pi-ai": "0.84.4",
111
+ "@earendil-works/pi-coding-agent": "0.84.4",
112
+ "@earendil-works/pi-tui": "0.84.4",
113
113
  "cron-parser": "^5.6.0",
114
114
  "esbuild": "^0.27.7",
115
115
  "proper-lockfile": "4.1.2",