@north-light/crouter 0.3.170 → 0.3.171
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/build-root.d.ts +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/insights/capture.md +66 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
- package/dist/clients/attach/__tests__/context-message.test.js +50 -3
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +35 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +7 -18
- package/dist/clients/attach/input/controller.d.ts +10 -0
- package/dist/clients/attach/input/controller.js +3 -0
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/chat-view.d.ts +81 -9
- package/dist/clients/attach/render/chat-view.js +434 -90
- package/dist/clients/attach/render/context-message.js +3 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/measured-container.d.ts +94 -0
- package/dist/clients/attach/render/measured-container.js +307 -0
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +160 -0
- package/dist/clients/attach/session/bindings.d.ts +5 -1
- package/dist/clients/attach/session/bindings.js +16 -0
- package/dist/clients/attach/session/context.d.ts +5 -3
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +173 -0
- package/dist/clients/attach/session/frames.d.ts +2 -0
- package/dist/clients/attach/session/frames.js +3 -0
- package/dist/clients/attach/session/input-wiring.d.ts +10 -4
- package/dist/clients/attach/session/input-wiring.js +13 -13
- package/dist/clients/attach/session/keys.d.ts +4 -1
- package/dist/clients/attach/session/keys.js +9 -4
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +32 -31
- package/dist/clients/attach/session/mouse.d.ts +15 -0
- package/dist/clients/attach/session/mouse.js +61 -0
- package/dist/clients/attach/slash/dispatch.d.ts +10 -0
- package/dist/clients/attach/slash/dispatch.js +35 -2
- package/dist/clients/attach/viewer.js +605 -581
- package/dist/clients/web/dev-server.d.ts +4 -1
- package/dist/clients/web/dev-server.js +8 -19
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/human/prompts.js +2 -1
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
- package/dist/commands/sys/setup-core.js +21 -16
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +1 -13
- package/dist/core/command.js +1 -16
- package/dist/core/config.js +2 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +8 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -1
- package/dist/core/runtime/broker.js +213 -5
- package/dist/core/runtime/canvas-extensions.d.ts +3 -0
- package/dist/core/runtime/canvas-extensions.js +3 -0
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +5 -2
- package/dist/core/runtime/node-read.js +15 -0
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +117 -0
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +91 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/user-settings.d.ts +14 -0
- package/dist/core/user-settings.js +20 -0
- package/dist/daemon/crtrd.js +5 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -2
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/pi-extensions/summary-tool.d.ts +33 -0
- package/dist/pi-extensions/summary-tool.js +62 -0
- package/dist/shared/generated-context.d.ts +19 -4
- package/dist/shared/generated-context.js +98 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +3 -1
- package/runtime.lock.json +62 -3
- package/scripts/postinstall.mjs +14 -1
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- package/dist/core/runtime/fault-recovery-nudge.js +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TERMRENDER_VERSION = "4.12.6";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TERMRENDER_VERSION = '4.12.6';
|
|
@@ -157,6 +157,20 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
|
|
|
157
157
|
setFromString: (config: ScopeConfig, raw: string) => string | null;
|
|
158
158
|
};
|
|
159
159
|
};
|
|
160
|
+
summarize_tool_calls: {
|
|
161
|
+
surface: "settings";
|
|
162
|
+
setting: {
|
|
163
|
+
key: "summarize_tool_calls";
|
|
164
|
+
label: string;
|
|
165
|
+
description: string;
|
|
166
|
+
valueConstraint: string;
|
|
167
|
+
applies: string;
|
|
168
|
+
order: number;
|
|
169
|
+
format: (config: ScopeConfig) => "off" | "on";
|
|
170
|
+
cycle: (config: ScopeConfig) => void;
|
|
171
|
+
setFromString: (config: ScopeConfig, raw: string) => string | null;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
160
174
|
keybindings: {
|
|
161
175
|
surface: "specialized";
|
|
162
176
|
owner: string;
|
|
@@ -223,6 +223,26 @@ export const CONFIG_SETTINGS_COVERAGE = {
|
|
|
223
223
|
},
|
|
224
224
|
},
|
|
225
225
|
},
|
|
226
|
+
summarize_tool_calls: {
|
|
227
|
+
surface: 'settings',
|
|
228
|
+
setting: {
|
|
229
|
+
key: 'summarize_tool_calls',
|
|
230
|
+
label: 'Summarize tool calls',
|
|
231
|
+
description: 'Replace completed folded tool-call groups with a one-line broker summary when available.',
|
|
232
|
+
valueConstraint: 'true|false',
|
|
233
|
+
applies: 'Applies when the next visible message closes a tool-call group.',
|
|
234
|
+
order: 35,
|
|
235
|
+
format: (config) => config.summarize_tool_calls ? 'on' : 'off',
|
|
236
|
+
cycle: (config) => { config.summarize_tool_calls = !config.summarize_tool_calls; },
|
|
237
|
+
setFromString: (config, raw) => {
|
|
238
|
+
const value = booleanFromString(raw, 'summarize_tool_calls');
|
|
239
|
+
if (typeof value === 'string')
|
|
240
|
+
return value;
|
|
241
|
+
config.summarize_tool_calls = value;
|
|
242
|
+
return null;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
226
246
|
keybindings: { surface: 'specialized', owner: 'Keybindings tab' },
|
|
227
247
|
modelLadders: { surface: 'specialized', owner: 'Models tab' },
|
|
228
248
|
modelRoutes: { surface: 'specialized', owner: 'Models route declarations' },
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -63,7 +63,7 @@ import { createApiServer } from './api/server.js';
|
|
|
63
63
|
// surface (tests, `commands/sys/daemon.ts`).
|
|
64
64
|
import { pidfilePath } from './pidfile.js';
|
|
65
65
|
export { readPidfile, isDaemonRunning } from './pidfile.js';
|
|
66
|
-
import {
|
|
66
|
+
import { rendererWarning } from '../core/runtime/package-health.js';
|
|
67
67
|
import { DEFAULT_INTERVAL_MS } from './supervise-cadence.js';
|
|
68
68
|
import { readConfig } from '../core/config.js';
|
|
69
69
|
export { FAULT_QUIET_MS };
|
|
@@ -1845,13 +1845,12 @@ export async function runDaemon(opts = {}) {
|
|
|
1845
1845
|
operationIdContext.fresh(() => {
|
|
1846
1846
|
emitEvent({ level: 'info', event: 'daemon.install_id.resolved', fields: { install_id: installId } });
|
|
1847
1847
|
});
|
|
1848
|
-
// The
|
|
1849
|
-
//
|
|
1850
|
-
// a stale executable without blocking supervision.
|
|
1848
|
+
// The pinned termrender renderer is an advisory dependency; setup repairs
|
|
1849
|
+
// an unprovisioned venv without blocking supervision.
|
|
1851
1850
|
operationIdContext.fresh(() => {
|
|
1852
1851
|
let warning;
|
|
1853
1852
|
try {
|
|
1854
|
-
warning =
|
|
1853
|
+
warning = rendererWarning();
|
|
1855
1854
|
}
|
|
1856
1855
|
catch {
|
|
1857
1856
|
return;
|
|
@@ -1860,7 +1859,7 @@ export async function runDaemon(opts = {}) {
|
|
|
1860
1859
|
emitEvent({
|
|
1861
1860
|
level: 'warn',
|
|
1862
1861
|
event: 'daemon.startup.warning',
|
|
1863
|
-
fields: { stage: '
|
|
1862
|
+
fields: { stage: 'termrender', warning: warning.slice(0, 2_048) },
|
|
1864
1863
|
});
|
|
1865
1864
|
}
|
|
1866
1865
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -24,4 +24,6 @@ export type { BuildEnvelopeInput, ErrorClass, ErrorClassCode, ErrorClassHints, E
|
|
|
24
24
|
export { validateCommandManifest } from './core/command-manifests/manifest.js';
|
|
25
25
|
export type { CommandManifestValidation, ValidatedCommandManifest } from './core/command-manifests/manifest.js';
|
|
26
26
|
export type { CommandManifestIssue, TransportKind } from './core/command-manifests/schema.js';
|
|
27
|
+
export { validatePluginBundle } from './core/command-plugins/bundle.js';
|
|
28
|
+
export type { BundleIssue, PluginBundleValidation, ValidatedBundle } from './core/command-plugins/bundle.js';
|
|
27
29
|
export { SUBTREE_NAMES } from './build-root.js';
|
package/dist/index.js
CHANGED
|
@@ -44,4 +44,5 @@ export { nowIso } from './core/fs-utils.js';
|
|
|
44
44
|
export { validateOperationId, buildEnvelope, operationIdContext, errorClassCodes, serializeEventRecord, errorClassFromError, } from './core/events/index.js';
|
|
45
45
|
// ── Command-manifest compatibility ───────────────────────────────────────
|
|
46
46
|
export { validateCommandManifest } from './core/command-manifests/manifest.js';
|
|
47
|
+
export { validatePluginBundle } from './core/command-plugins/bundle.js';
|
|
47
48
|
export { SUBTREE_NAMES } from './build-root.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/pi-extensions/__tests__/canvas-structured-output.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Regression: Codex strict function schemas require every property in `required`.
|
|
4
|
+
import { after, test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { convertResponsesTools } from '@earendil-works/pi-ai/api/openai-responses-shared';
|
|
10
|
+
import { writeOutputSchema } from '../../core/runtime/structured-output.js';
|
|
11
|
+
import registerCanvasStructuredOutput from '../canvas-structured-output.js';
|
|
12
|
+
const previous = {
|
|
13
|
+
home: process.env['CRTR_HOME'],
|
|
14
|
+
nodeId: process.env['CRTR_NODE_ID'],
|
|
15
|
+
};
|
|
16
|
+
let home;
|
|
17
|
+
after(() => {
|
|
18
|
+
if (home !== undefined)
|
|
19
|
+
rmSync(home, { recursive: true, force: true });
|
|
20
|
+
if (previous.home === undefined)
|
|
21
|
+
delete process.env['CRTR_HOME'];
|
|
22
|
+
else
|
|
23
|
+
process.env['CRTR_HOME'] = previous.home;
|
|
24
|
+
if (previous.nodeId === undefined)
|
|
25
|
+
delete process.env['CRTR_NODE_ID'];
|
|
26
|
+
else
|
|
27
|
+
process.env['CRTR_NODE_ID'] = previous.nodeId;
|
|
28
|
+
});
|
|
29
|
+
test('optional submit fields remain optional through a strict provider tool schema', () => {
|
|
30
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-structured-output-'));
|
|
31
|
+
process.env['CRTR_HOME'] = home;
|
|
32
|
+
process.env['CRTR_NODE_ID'] = 'structured-output-test';
|
|
33
|
+
const schema = {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
status: { type: 'string' },
|
|
37
|
+
commit: { type: 'string' },
|
|
38
|
+
verification: { type: 'string' },
|
|
39
|
+
blocker: { type: 'string' },
|
|
40
|
+
},
|
|
41
|
+
required: ['status', 'verification'],
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
};
|
|
44
|
+
writeOutputSchema('structured-output-test', 'oneoff', schema);
|
|
45
|
+
let definition;
|
|
46
|
+
registerCanvasStructuredOutput({
|
|
47
|
+
registerTool(tool) {
|
|
48
|
+
definition = tool;
|
|
49
|
+
},
|
|
50
|
+
on() { },
|
|
51
|
+
});
|
|
52
|
+
assert.ok(definition, 'the pending request registers submit');
|
|
53
|
+
const parameters = definition['parameters'];
|
|
54
|
+
assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker']);
|
|
55
|
+
assert.deepEqual(parameters.properties.commit, { anyOf: [{ type: 'string' }, { type: 'null' }] });
|
|
56
|
+
assert.deepEqual(parameters.properties.blocker, { anyOf: [{ type: 'string' }, { type: 'null' }] });
|
|
57
|
+
assert.deepEqual(schema.required, ['status', 'verification'], 'the stored request keeps its original optionality');
|
|
58
|
+
const [providerTool] = convertResponsesTools([definition], {
|
|
59
|
+
strict: false,
|
|
60
|
+
supportsStrictMode: true,
|
|
61
|
+
});
|
|
62
|
+
assert.equal(providerTool['strict'], true);
|
|
63
|
+
});
|
|
@@ -44,7 +44,7 @@ import { clearStopSignals, consumeStopSignals } from '../core/runtime/stop-signa
|
|
|
44
44
|
import { cliClient } from '../commands/api-client.js';
|
|
45
45
|
import { focusOf, tearDownNode } from '../core/runtime/placement.js';
|
|
46
46
|
import { readCursor, readInboxSince } from '../core/feed/inbox.js';
|
|
47
|
-
import { CONTEXT_NUDGE_CUSTOM_TYPE, formatContextNudge } from '../shared/generated-context.js';
|
|
47
|
+
import { CONTEXT_NUDGE_CUSTOM_TYPE, formatContextNudge, formatPersonaTransition } from '../shared/generated-context.js';
|
|
48
48
|
/** Universal upper bound for an unfocused broker parked without runnable work. */
|
|
49
49
|
export const UNATTENDED_PARK_MS = (() => {
|
|
50
50
|
const testOverride = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
@@ -720,7 +720,7 @@ export function registerCanvasStophook(pi) {
|
|
|
720
720
|
try {
|
|
721
721
|
const drift = personaDrift(nodeId);
|
|
722
722
|
if (drift !== null) {
|
|
723
|
-
pi.sendUserMessage(drift.guidance, { deliverAs: 'steer' });
|
|
723
|
+
pi.sendUserMessage(formatPersonaTransition(drift.guidance), { deliverAs: 'steer' });
|
|
724
724
|
commitPersonaAck(nodeId, drift.to);
|
|
725
725
|
}
|
|
726
726
|
}
|
|
@@ -27,6 +27,50 @@ function retireTool(pi) {
|
|
|
27
27
|
/* best-effort; the execute handler is inert when no schema file exists */
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
function strictToolSchema(schema) {
|
|
31
|
+
const transform = (value) => {
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value.map(transform);
|
|
34
|
+
if (value === null || typeof value !== 'object')
|
|
35
|
+
return value;
|
|
36
|
+
const source = value;
|
|
37
|
+
const transformed = {};
|
|
38
|
+
for (const [key, child] of Object.entries(source)) {
|
|
39
|
+
if (key === 'properties' && child !== null && typeof child === 'object' && !Array.isArray(child)) {
|
|
40
|
+
transformed[key] = Object.fromEntries(Object.entries(child).map(([name, property]) => [name, transform(property)]));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
transformed[key] = transform(child);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const properties = transformed['properties'];
|
|
47
|
+
if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
|
|
48
|
+
return transformed;
|
|
49
|
+
const required = new Set(Array.isArray(source['required']) ? source['required'].filter((name) => typeof name === 'string') : []);
|
|
50
|
+
transformed['properties'] = Object.fromEntries(Object.entries(properties).map(([name, property]) => [
|
|
51
|
+
name,
|
|
52
|
+
required.has(name) ? property : { anyOf: [property, { type: 'null' }] },
|
|
53
|
+
]));
|
|
54
|
+
transformed['required'] = Object.keys(properties);
|
|
55
|
+
return transformed;
|
|
56
|
+
};
|
|
57
|
+
return transform(schema);
|
|
58
|
+
}
|
|
59
|
+
function omitStrictNulls(value, schema) {
|
|
60
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || schema === null || typeof schema !== 'object' || Array.isArray(schema))
|
|
61
|
+
return value;
|
|
62
|
+
const source = schema;
|
|
63
|
+
const properties = source['properties'];
|
|
64
|
+
if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
|
|
65
|
+
return value;
|
|
66
|
+
const requiredNames = source['required'];
|
|
67
|
+
const required = new Set(Array.isArray(requiredNames) ? requiredNames.filter((name) => typeof name === 'string') : []);
|
|
68
|
+
return Object.fromEntries(Object.entries(value).flatMap(([name, child]) => {
|
|
69
|
+
if (child === null && !required.has(name) && Object.hasOwn(properties, name))
|
|
70
|
+
return [];
|
|
71
|
+
return [[name, omitStrictNulls(child, properties[name])]];
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
30
74
|
function toolDescription(request) {
|
|
31
75
|
if (request.mode === 'terminal') {
|
|
32
76
|
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.';
|
|
@@ -66,11 +110,10 @@ export function registerCanvasStructuredOutput(pi) {
|
|
|
66
110
|
'Use submit to answer the pending one-off structured-output request before stopping or going dormant.',
|
|
67
111
|
'After one-off submit succeeds, continue with any remaining work normally.',
|
|
68
112
|
],
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
// stay authoritative either way — this only makes a malformed submission rarer.
|
|
113
|
+
// OpenAI strict tools require every property to be required. Optional fields
|
|
114
|
+
// become nullable in the provider-only schema and null is removed before the
|
|
115
|
+
// original result is persisted.
|
|
116
|
+
parameters: strictToolSchema(request.schema),
|
|
74
117
|
constrainedSampling: { type: 'json_schema', strict: 'prefer' },
|
|
75
118
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
76
119
|
const current = readOutputRequest(nodeId);
|
|
@@ -88,8 +131,9 @@ export function registerCanvasStructuredOutput(pi) {
|
|
|
88
131
|
details: { submitted: false, reason: 'schema_replaced' },
|
|
89
132
|
};
|
|
90
133
|
}
|
|
134
|
+
const result = omitStrictNulls(params, current.schema);
|
|
91
135
|
const resultPath = outputResultPath(nodeId);
|
|
92
|
-
const json = JSON.stringify(
|
|
136
|
+
const json = JSON.stringify(result, null, 2);
|
|
93
137
|
mkdirSync(dirname(resultPath), { recursive: true });
|
|
94
138
|
writeFileSync(resultPath, `${json}\n`, 'utf8');
|
|
95
139
|
const body = reportBody(resultPath, json);
|
|
@@ -114,7 +158,7 @@ export function registerCanvasStructuredOutput(pi) {
|
|
|
114
158
|
catch { /* ignore */ }
|
|
115
159
|
return {
|
|
116
160
|
content: [{ type: 'text', text: `Structured output submitted and finalized: ${resultPath}` }],
|
|
117
|
-
details: { submitted: true, mode: current.mode, resultPath, result
|
|
161
|
+
details: { submitted: true, mode: current.mode, resultPath, result },
|
|
118
162
|
terminate: true,
|
|
119
163
|
};
|
|
120
164
|
}
|
|
@@ -124,7 +168,7 @@ export function registerCanvasStructuredOutput(pi) {
|
|
|
124
168
|
retireTool(pi);
|
|
125
169
|
return {
|
|
126
170
|
content: [{ type: 'text', text: `Structured output submitted: ${resultPath}` }],
|
|
127
|
-
details: { submitted: true, mode: current.mode, resultPath, result
|
|
171
|
+
details: { submitted: true, mode: current.mode, resultPath, result },
|
|
128
172
|
};
|
|
129
173
|
},
|
|
130
174
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface PiLike {
|
|
2
|
+
registerTool: (definition: Record<string, unknown>) => void;
|
|
3
|
+
}
|
|
4
|
+
export declare const SUBMIT_SUMMARY_SCHEMA: {
|
|
5
|
+
readonly type: "object";
|
|
6
|
+
readonly properties: {
|
|
7
|
+
readonly bullets: {
|
|
8
|
+
readonly type: "array";
|
|
9
|
+
readonly description: "One to five very short, plain-English outcome strings without bullet markers, identifiers, or operational bookkeeping.";
|
|
10
|
+
readonly minItems: 1;
|
|
11
|
+
readonly maxItems: 5;
|
|
12
|
+
readonly items: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly minLength: 1;
|
|
15
|
+
readonly maxLength: 120;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
readonly nodesSpawned: {
|
|
19
|
+
readonly type: "integer";
|
|
20
|
+
readonly minimum: 0;
|
|
21
|
+
readonly description: "Count of nodes created during the supplied segment, or zero when none are evidenced.";
|
|
22
|
+
};
|
|
23
|
+
readonly filesEdited: {
|
|
24
|
+
readonly type: "integer";
|
|
25
|
+
readonly minimum: 0;
|
|
26
|
+
readonly description: "Count of distinct files created, modified, or deleted during the supplied segment, or zero when none are evidenced.";
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
readonly required: readonly ["bullets", "nodesSpawned", "filesEdited"];
|
|
30
|
+
readonly additionalProperties: false;
|
|
31
|
+
};
|
|
32
|
+
export declare function registerSummaryTool(pi: PiLike): void;
|
|
33
|
+
export default registerSummaryTool;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// summary-tool.ts — structured-output seam for broker-owned tool-group summaries.
|
|
2
|
+
// Loaded only by the headless summary subprocess. A file is the return channel
|
|
3
|
+
// because pi print-mode owns stdout.
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
export const SUBMIT_SUMMARY_SCHEMA = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
bullets: {
|
|
10
|
+
type: 'array',
|
|
11
|
+
description: 'One to five very short, plain-English outcome strings without bullet markers, identifiers, or operational bookkeeping.',
|
|
12
|
+
minItems: 1,
|
|
13
|
+
maxItems: 5,
|
|
14
|
+
items: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
minLength: 1,
|
|
17
|
+
maxLength: 120,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
nodesSpawned: {
|
|
21
|
+
type: 'integer',
|
|
22
|
+
minimum: 0,
|
|
23
|
+
description: 'Count of nodes created during the supplied segment, or zero when none are evidenced.',
|
|
24
|
+
},
|
|
25
|
+
filesEdited: {
|
|
26
|
+
type: 'integer',
|
|
27
|
+
minimum: 0,
|
|
28
|
+
description: 'Count of distinct files created, modified, or deleted during the supplied segment, or zero when none are evidenced.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['bullets', 'nodesSpawned', 'filesEdited'],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
};
|
|
34
|
+
export function registerSummaryTool(pi) {
|
|
35
|
+
const out = process.env['CRTR_SUMMARY_RESULT_FILE'];
|
|
36
|
+
if (out === undefined || out.trim() === '')
|
|
37
|
+
return;
|
|
38
|
+
pi.registerTool({
|
|
39
|
+
name: 'submit_summary',
|
|
40
|
+
label: 'Submit summary',
|
|
41
|
+
description: 'Submit the structured tool-group activity recap exactly once, then stop.',
|
|
42
|
+
parameters: SUBMIT_SUMMARY_SCHEMA,
|
|
43
|
+
// Anthropic's constrained-sampling subset rejects the array/range keywords
|
|
44
|
+
// this contract requires; ordinary tool calling accepts the complete schema.
|
|
45
|
+
constrainedSampling: false,
|
|
46
|
+
execute(_id, params) {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
49
|
+
writeFileSync(out, JSON.stringify(params ?? {}), 'utf8');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// A missing result is a failed summary; callers never invent one.
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: 'text', text: 'Recorded.' }],
|
|
56
|
+
details: { recorded: true },
|
|
57
|
+
terminate: true,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export default registerSummaryTool;
|
|
@@ -8,13 +8,28 @@ export declare const CONTEXT_NUDGE_CUSTOM_TYPE = "crtr-context-nudge";
|
|
|
8
8
|
export declare const REVIVE_KICKOFF_SENTINEL = "You have been revived fresh after a context refresh";
|
|
9
9
|
/** Prefix shared by the producer and the compatibility classifier for context nudges. */
|
|
10
10
|
export declare const CONTEXT_NUDGE_PREFIX = "[crtr] Context ~";
|
|
11
|
+
/** Runtime-authored continuation after a cleanly aborted turn survives a restart. */
|
|
12
|
+
export declare const RUNTIME_RESTART_CONTINUATION = "<runtime-restart-continuation>\ncontinue\n</runtime-restart-continuation>";
|
|
13
|
+
/** Generic completion mandate issued by the terminal-node stop guard. */
|
|
14
|
+
export declare const STALL_REPROMPT: string;
|
|
15
|
+
/** Static recovery prompts shared by the broker producer and display classifier. */
|
|
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.";
|
|
17
|
+
export declare 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.";
|
|
18
|
+
export declare const PROVIDER_FAULT_RECOVERY_BODY = "Your previous turn stopped on a provider fault. Continue from where you left off and retry the work that failed.";
|
|
19
|
+
/** Mark an active persona transition without changing the guidance inside it. */
|
|
20
|
+
export declare function formatPersonaTransition(guidance: string): string;
|
|
21
|
+
/** Format the stop guard's dynamic structured-output mandate. */
|
|
22
|
+
export declare function formatStructuredOutputReprompt(schema: string): string;
|
|
23
|
+
export type ModelFallbackRecoveryReason = 'credential' | 'not-found';
|
|
24
|
+
/** Mark a model-route fallback re-drive while retaining its full guidance. */
|
|
25
|
+
export declare function formatModelFallbackRecovery(previousModel: string, nextModel: string, reason: ModelFallbackRecoveryReason): string;
|
|
11
26
|
export interface GeneratedContextMessageLike {
|
|
12
27
|
role?: string;
|
|
13
28
|
customType?: string;
|
|
14
29
|
content?: unknown;
|
|
15
30
|
}
|
|
16
31
|
export interface GeneratedContextPresentation {
|
|
17
|
-
kind: 'bearings' | 'situational' | 'revive' | 'context-nudge' | 'inbox';
|
|
32
|
+
kind: 'bearings' | 'situational' | 'revive' | 'context-nudge' | 'inbox' | 'persona-transition' | 'stop-guard' | 'recovery';
|
|
18
33
|
label: string;
|
|
19
34
|
summary: string;
|
|
20
35
|
body: string;
|
|
@@ -27,8 +42,8 @@ export declare function isInboxDigest(text: string): boolean;
|
|
|
27
42
|
export declare function extractInboxSender(text: string): string | null;
|
|
28
43
|
/**
|
|
29
44
|
* Classify a crouter-generated context message for display. The checks are
|
|
30
|
-
* exact producer-owned contracts: custom messages use their customType
|
|
31
|
-
* user
|
|
32
|
-
* Ordinary user prompts
|
|
45
|
+
* exact producer-owned contracts: custom messages use their customType, and
|
|
46
|
+
* user messages use stable sentinels or the same exact formatter/constants as
|
|
47
|
+
* their producers. Ordinary user prompts remain ordinary user prompts.
|
|
33
48
|
*/
|
|
34
49
|
export declare function generatedContextPresentation(message: GeneratedContextMessageLike): GeneratedContextPresentation | null;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// generated-context.ts — the display contract for crouter-authored context messages.
|
|
2
2
|
//
|
|
3
3
|
// The runtime sends these messages in the role that preserves their delivery
|
|
4
|
-
// semantics: fresh-revive kickoffs, mid-run nudges,
|
|
5
|
-
//
|
|
4
|
+
// semantics: fresh-revive kickoffs, mid-run nudges, persona transitions, stop
|
|
5
|
+
// guards, recovery prompts, and node inbox digests are user messages, while
|
|
6
|
+
// bearings, situational updates, and held nudges are custom messages. Attached
|
|
6
7
|
// views should still present all of them as generated context rather than as
|
|
7
|
-
// ordinary human prompts. This pure module owns their stable identifiers
|
|
8
|
-
// display classification so the terminal and web
|
|
8
|
+
// ordinary human prompts. This pure module owns their stable identifiers,
|
|
9
|
+
// producer formatting, and display classification so the terminal and web
|
|
10
|
+
// renderers cannot drift.
|
|
9
11
|
/** Custom message carrying the node's session-start bearings. */
|
|
10
12
|
export const CONTEXT_INTRO_CUSTOM_TYPE = 'crtr-context';
|
|
11
13
|
/** Custom message carrying an ambient situational-context update. */
|
|
@@ -16,6 +18,36 @@ export const CONTEXT_NUDGE_CUSTOM_TYPE = 'crtr-context-nudge';
|
|
|
16
18
|
export const REVIVE_KICKOFF_SENTINEL = 'You have been revived fresh after a context refresh';
|
|
17
19
|
/** Prefix shared by the producer and the compatibility classifier for context nudges. */
|
|
18
20
|
export const CONTEXT_NUDGE_PREFIX = '[crtr] Context ~';
|
|
21
|
+
/** Runtime-authored continuation after a cleanly aborted turn survives a restart. */
|
|
22
|
+
export const RUNTIME_RESTART_CONTINUATION = '<runtime-restart-continuation>\ncontinue\n</runtime-restart-continuation>';
|
|
23
|
+
/** Generic completion mandate issued by the terminal-node stop guard. */
|
|
24
|
+
export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
|
|
25
|
+
"Pipe the result to `crtr push final` through a single-quoted heredoc if the work is done, or use `crtr human ask` if you are blocked or need the user.";
|
|
26
|
+
/** Static recovery prompts shared by the broker producer and display classifier. */
|
|
27
|
+
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.';
|
|
28
|
+
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.';
|
|
29
|
+
export const PROVIDER_FAULT_RECOVERY_BODY = 'Your previous turn stopped on a provider fault. Continue from where you left off and retry the work that failed.';
|
|
30
|
+
const PERSONA_TRANSITION_OPEN = '<persona-transition>';
|
|
31
|
+
const PERSONA_TRANSITION_CLOSE = '</persona-transition>';
|
|
32
|
+
const MODEL_FALLBACK_RECOVERY_OPEN = '<model-fallback-recovery>';
|
|
33
|
+
const MODEL_FALLBACK_RECOVERY_CLOSE = '</model-fallback-recovery>';
|
|
34
|
+
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';
|
|
35
|
+
const STRUCTURED_OUTPUT_REPROMPT_SUFFIX = '\n```';
|
|
36
|
+
/** Mark an active persona transition without changing the guidance inside it. */
|
|
37
|
+
export function formatPersonaTransition(guidance) {
|
|
38
|
+
return `${PERSONA_TRANSITION_OPEN}\n${guidance}\n${PERSONA_TRANSITION_CLOSE}`;
|
|
39
|
+
}
|
|
40
|
+
/** Format the stop guard's dynamic structured-output mandate. */
|
|
41
|
+
export function formatStructuredOutputReprompt(schema) {
|
|
42
|
+
return `${STRUCTURED_OUTPUT_REPROMPT_PREFIX}${schema}${STRUCTURED_OUTPUT_REPROMPT_SUFFIX}`;
|
|
43
|
+
}
|
|
44
|
+
/** Mark a model-route fallback re-drive while retaining its full guidance. */
|
|
45
|
+
export function formatModelFallbackRecovery(previousModel, nextModel, reason) {
|
|
46
|
+
const guidance = reason === 'credential'
|
|
47
|
+
? `The previous model (${previousModel}) had no usable provider credential, so you were automatically switched to ${nextModel}. Continue the task from where the failed turn left off.`
|
|
48
|
+
: `The previous model (${previousModel}) was unavailable (provider 404 not_found), so you were automatically switched to ${nextModel}. Continue the task from where the failed turn left off.`;
|
|
49
|
+
return `${MODEL_FALLBACK_RECOVERY_OPEN}\n${guidance}\n${MODEL_FALLBACK_RECOVERY_CLOSE}`;
|
|
50
|
+
}
|
|
19
51
|
/** Plain text from a generated message's string or text-block content. */
|
|
20
52
|
export function generatedContextText(message) {
|
|
21
53
|
if (typeof message.content === 'string')
|
|
@@ -58,11 +90,50 @@ function inboxSummary(body) {
|
|
|
58
90
|
}
|
|
59
91
|
return `${updates} updates from ${headers.length} senders`;
|
|
60
92
|
}
|
|
93
|
+
function isPersonaTransition(body) {
|
|
94
|
+
return body.startsWith(`${PERSONA_TRANSITION_OPEN}\n`) && body.endsWith(`\n${PERSONA_TRANSITION_CLOSE}`);
|
|
95
|
+
}
|
|
96
|
+
function isStructuredOutputReprompt(body) {
|
|
97
|
+
if (!body.startsWith(STRUCTURED_OUTPUT_REPROMPT_PREFIX) || !body.endsWith(STRUCTURED_OUTPUT_REPROMPT_SUFFIX)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const schema = body.slice(STRUCTURED_OUTPUT_REPROMPT_PREFIX.length, -STRUCTURED_OUTPUT_REPROMPT_SUFFIX.length);
|
|
101
|
+
try {
|
|
102
|
+
JSON.parse(schema);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const MODEL_CREDENTIAL_RECOVERY_RE = /^The previous model \([^\n]+\) had no usable provider credential, so you were automatically switched to [^\n]+\. Continue the task from where the failed turn left off\.$/;
|
|
110
|
+
const MODEL_NOT_FOUND_RECOVERY_RE = /^The previous model \([^\n]+\) was unavailable \(provider 404 not_found\), so you were automatically switched to [^\n]+\. Continue the task from where the failed turn left off\.$/;
|
|
111
|
+
function isModelFallbackRecovery(body) {
|
|
112
|
+
if (!body.startsWith(`${MODEL_FALLBACK_RECOVERY_OPEN}\n`)
|
|
113
|
+
|| !body.endsWith(`\n${MODEL_FALLBACK_RECOVERY_CLOSE}`)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const guidance = body.slice(MODEL_FALLBACK_RECOVERY_OPEN.length + 1, -(MODEL_FALLBACK_RECOVERY_CLOSE.length + 1));
|
|
117
|
+
return MODEL_CREDENTIAL_RECOVERY_RE.test(guidance) || MODEL_NOT_FOUND_RECOVERY_RE.test(guidance);
|
|
118
|
+
}
|
|
119
|
+
function recoverySummary(body) {
|
|
120
|
+
if (body === AUTH_FAULT_RECOVERY_BODY)
|
|
121
|
+
return 'provider credentials updated';
|
|
122
|
+
if (body === CONNECTION_FAULT_RECOVERY_BODY)
|
|
123
|
+
return 'network connection restored';
|
|
124
|
+
if (body === PROVIDER_FAULT_RECOVERY_BODY)
|
|
125
|
+
return 'provider retry';
|
|
126
|
+
if (body === RUNTIME_RESTART_CONTINUATION)
|
|
127
|
+
return 'runtime restarted';
|
|
128
|
+
if (isModelFallbackRecovery(body))
|
|
129
|
+
return 'model route changed';
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
61
132
|
/**
|
|
62
133
|
* Classify a crouter-generated context message for display. The checks are
|
|
63
|
-
* exact producer-owned contracts: custom messages use their customType
|
|
64
|
-
* user
|
|
65
|
-
* Ordinary user prompts
|
|
134
|
+
* exact producer-owned contracts: custom messages use their customType, and
|
|
135
|
+
* user messages use stable sentinels or the same exact formatter/constants as
|
|
136
|
+
* their producers. Ordinary user prompts remain ordinary user prompts.
|
|
66
137
|
*/
|
|
67
138
|
export function generatedContextPresentation(message) {
|
|
68
139
|
const body = generatedContextText(message);
|
|
@@ -94,5 +165,25 @@ export function generatedContextPresentation(message) {
|
|
|
94
165
|
if (isInboxDigest(body)) {
|
|
95
166
|
return { kind: 'inbox', label: 'crtr inbox', summary: inboxSummary(body), body };
|
|
96
167
|
}
|
|
168
|
+
if (isPersonaTransition(body)) {
|
|
169
|
+
return {
|
|
170
|
+
kind: 'persona-transition',
|
|
171
|
+
label: 'crtr persona',
|
|
172
|
+
summary: 'runtime role update',
|
|
173
|
+
body,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (body === STALL_REPROMPT || isStructuredOutputReprompt(body)) {
|
|
177
|
+
return {
|
|
178
|
+
kind: 'stop-guard',
|
|
179
|
+
label: 'crtr stop guard',
|
|
180
|
+
summary: body === STALL_REPROMPT ? 'completion required' : 'structured output required',
|
|
181
|
+
body,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const recovery = recoverySummary(body);
|
|
185
|
+
if (recovery !== null) {
|
|
186
|
+
return { kind: 'recovery', label: 'crtr recovery', summary: recovery, body };
|
|
187
|
+
}
|
|
97
188
|
return null;
|
|
98
189
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Broker-generated activity recap for one completed tool-call group. */
|
|
2
|
+
export interface ToolGroupSummary {
|
|
3
|
+
bullets: string[];
|
|
4
|
+
nodesSpawned: number;
|
|
5
|
+
filesEdited: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ToolGroupMessageLike {
|
|
8
|
+
role?: string;
|
|
9
|
+
content?: unknown;
|
|
10
|
+
customType?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Validate the one durable summary shape and cap its display-sized fields.
|
|
13
|
+
* Invalid entries are absent rather than adapted, so their calls stay visible. */
|
|
14
|
+
export declare function normalizeToolGroupSummary(value: unknown): ToolGroupSummary | null;
|
|
15
|
+
/** Validate sidecar/snapshot entries independently so one malformed or legacy
|
|
16
|
+
* value never collapses any otherwise valid group. */
|
|
17
|
+
export declare function normalizeToolGroupSummaryRecord(value: unknown): Record<string, ToolGroupSummary>;
|
|
18
|
+
/** Text a renderer presents as an assistant bubble. Thinking and tool-call
|
|
19
|
+
* blocks deliberately do not count: only visible prose establishes a boundary. */
|
|
20
|
+
export declare function assistantVisibleText(message: ToolGroupMessageLike): string;
|
|
21
|
+
/** A person-authored user message closes the preceding group. Runtime-generated
|
|
22
|
+
* user context retains role=user for delivery semantics but is not human prose. */
|
|
23
|
+
export declare function isTrueUserMessage(message: ToolGroupMessageLike): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Shared transcript-boundary rules for broker-owned tool-group summaries.
|
|
2
|
+
// Keep this independent of renderer state: both live event tracking and snapshot
|
|
3
|
+
// reconstruction must agree on which prose closes a run of tools.
|
|
4
|
+
import { generatedContextPresentation, generatedContextText } from './generated-context.js';
|
|
5
|
+
const TOOL_GROUP_SUMMARY_KEYS = new Set(['bullets', 'nodesSpawned', 'filesEdited']);
|
|
6
|
+
const MAX_TOOL_GROUP_BULLETS = 5;
|
|
7
|
+
const MAX_TOOL_GROUP_BULLET_LENGTH = 1_000;
|
|
8
|
+
/** Validate the one durable summary shape and cap its display-sized fields.
|
|
9
|
+
* Invalid entries are absent rather than adapted, so their calls stay visible. */
|
|
10
|
+
export function normalizeToolGroupSummary(value) {
|
|
11
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
12
|
+
return null;
|
|
13
|
+
const record = value;
|
|
14
|
+
const keys = Object.keys(record);
|
|
15
|
+
if (keys.length !== TOOL_GROUP_SUMMARY_KEYS.size || !keys.every((key) => TOOL_GROUP_SUMMARY_KEYS.has(key))) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(record.bullets) || record.bullets.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
if (!record.bullets.every((bullet) => typeof bullet === 'string' && bullet.trim() !== ''))
|
|
21
|
+
return null;
|
|
22
|
+
if (typeof record.nodesSpawned !== 'number' || !Number.isSafeInteger(record.nodesSpawned) || record.nodesSpawned < 0)
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof record.filesEdited !== 'number' || !Number.isSafeInteger(record.filesEdited) || record.filesEdited < 0)
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
bullets: record.bullets
|
|
28
|
+
.slice(0, MAX_TOOL_GROUP_BULLETS)
|
|
29
|
+
.map((bullet) => bullet.trim().slice(0, MAX_TOOL_GROUP_BULLET_LENGTH).trimEnd()),
|
|
30
|
+
nodesSpawned: record.nodesSpawned,
|
|
31
|
+
filesEdited: record.filesEdited,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Validate sidecar/snapshot entries independently so one malformed or legacy
|
|
35
|
+
* value never collapses any otherwise valid group. */
|
|
36
|
+
export function normalizeToolGroupSummaryRecord(value) {
|
|
37
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
38
|
+
return {};
|
|
39
|
+
const summaries = {};
|
|
40
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
41
|
+
if (key.trim() === '')
|
|
42
|
+
continue;
|
|
43
|
+
const summary = normalizeToolGroupSummary(candidate);
|
|
44
|
+
if (summary !== null)
|
|
45
|
+
summaries[key] = summary;
|
|
46
|
+
}
|
|
47
|
+
return summaries;
|
|
48
|
+
}
|
|
49
|
+
/** Text a renderer presents as an assistant bubble. Thinking and tool-call
|
|
50
|
+
* blocks deliberately do not count: only visible prose establishes a boundary. */
|
|
51
|
+
export function assistantVisibleText(message) {
|
|
52
|
+
if (message.role !== 'assistant')
|
|
53
|
+
return '';
|
|
54
|
+
return generatedContextText(message);
|
|
55
|
+
}
|
|
56
|
+
/** A person-authored user message closes the preceding group. Runtime-generated
|
|
57
|
+
* user context retains role=user for delivery semantics but is not human prose. */
|
|
58
|
+
export function isTrueUserMessage(message) {
|
|
59
|
+
return message.role === 'user' && generatedContextPresentation(message) === null;
|
|
60
|
+
}
|