@north-light/crouter 0.3.204 → 0.3.206
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/dto/broker.d.ts +2 -0
- package/dist/api/dto/messages.d.ts +22 -1
- package/dist/api/dto/nodes.d.ts +8 -5
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +54 -19
- package/dist/clients/attach/__tests__/page-block.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/page-block.test.js +54 -0
- package/dist/clients/attach/render/chat-view.d.ts +3 -0
- package/dist/clients/attach/render/chat-view.js +56 -27
- package/dist/clients/attach/render/context-message.d.ts +6 -0
- package/dist/clients/attach/render/context-message.js +21 -3
- package/dist/clients/attach/render/group-activity.d.ts +5 -3
- package/dist/clients/attach/render/group-activity.js +10 -6
- package/dist/clients/attach/render/page-block.js +11 -11
- package/dist/clients/attach/viewer.js +717 -703
- package/dist/clients/conversation/projection.js +7 -37
- package/dist/commands/node/create.js +1 -1
- package/dist/commands/profile/project.js +1 -1
- package/dist/commands/profile/show.js +1 -1
- package/dist/commands/profile.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +90 -23
- package/dist/core/__tests__/context-intro.test.js +7 -8
- package/dist/core/__tests__/human-deliver.test.js +32 -4
- package/dist/core/__tests__/kickoff.test.js +6 -6
- package/dist/core/__tests__/serial/broker-snapshot-history.test.js +5 -2
- package/dist/core/__tests__/serial/deferred-no-wake.test.js +7 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +4 -1
- package/dist/core/__tests__/serial/revive.test.js +8 -3
- package/dist/core/__tests__/session-cycles.test.js +10 -6
- package/dist/core/canvas/render-source.js +3 -0
- package/dist/core/feed/feed.d.ts +7 -1
- package/dist/core/feed/feed.js +8 -7
- package/dist/core/feed/inbox.d.ts +16 -12
- package/dist/core/feed/inbox.js +56 -56
- package/dist/core/human/page-eval.d.ts +9 -4
- package/dist/core/human/page-eval.js +3 -1
- package/dist/core/human/page-markdown.d.ts +3 -0
- package/dist/core/human/page-markdown.js +295 -0
- package/dist/core/human/page.js +4 -0
- package/dist/core/runtime/bearings.js +5 -4
- package/dist/core/runtime/broker/client-registry.d.ts +1 -0
- package/dist/core/runtime/broker/client-registry.js +1 -0
- package/dist/core/runtime/broker/fault-retry.js +8 -3
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +13 -2
- package/dist/core/runtime/broker/inbox.d.ts +20 -2
- package/dist/core/runtime/broker/inbox.js +68 -25
- package/dist/core/runtime/broker/passive.d.ts +1 -0
- package/dist/core/runtime/broker/tool-groups.js +3 -2
- package/dist/core/runtime/broker-extension-render.d.ts +3 -0
- package/dist/core/runtime/broker-extension-render.js +3 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker-protocol.js +11 -0
- package/dist/core/runtime/broker.d.ts +2 -2
- package/dist/core/runtime/broker.js +31 -6
- package/dist/core/runtime/deliver-live.js +2 -1
- package/dist/core/runtime/interactive-deliver.d.ts +3 -2
- package/dist/core/runtime/interactive-deliver.js +4 -3
- package/dist/core/runtime/kickoff.d.ts +3 -4
- package/dist/core/runtime/kickoff.js +27 -16
- package/dist/core/runtime/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +11 -3
- package/dist/core/runtime/session-cycles.d.ts +6 -1
- package/dist/core/runtime/session-cycles.js +17 -11
- package/dist/core/runtime/session-visibility.d.ts +7 -12
- package/dist/core/runtime/session-visibility.js +7 -13
- package/dist/core/runtime/situational-context.d.ts +14 -5
- package/dist/core/runtime/situational-context.js +39 -21
- package/dist/core/runtime/situational-live.d.ts +2 -2
- package/dist/core/runtime/situational-live.js +17 -16
- package/dist/core/runtime/spawn.js +4 -3
- package/dist/core/runtime/stop-guard.js +8 -4
- package/dist/core/runtime/warm-pool.d.ts +1 -1
- package/dist/core/runtime/warm-pool.js +6 -9
- package/dist/daemon/api/handlers/messages.js +101 -17
- package/dist/daemon/api/handlers/nodes.js +12 -8
- package/dist/daemon/human/finish.js +44 -4
- package/dist/daemon/messaging/node-message.d.ts +5 -0
- package/dist/daemon/messaging/node-message.js +6 -1
- package/dist/daemon/review/comment-notify.js +8 -0
- package/dist/daemon/review/deliver.js +2 -2
- package/dist/daemon/review/finish.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +96 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +2 -2
- package/dist/pi-extensions/broker-local.d.ts +0 -2
- package/dist/pi-extensions/broker-local.js +0 -2
- package/dist/pi-extensions/canvas-context-intro.js +9 -8
- package/dist/pi-extensions/canvas-goal-capture.js +10 -6
- package/dist/pi-extensions/canvas-inbox-watcher.js +116 -69
- package/dist/pi-extensions/canvas-passive-context.d.ts +10 -0
- package/dist/pi-extensions/canvas-passive-context.js +23 -3
- package/dist/pi-extensions/canvas-review-boundary.d.ts +1 -1
- package/dist/pi-extensions/canvas-review-boundary.js +11 -3
- package/dist/pi-extensions/canvas-stophook.js +3 -3
- package/dist/shared/__tests__/generated-context-grammar.test.d.ts +1 -0
- package/dist/shared/__tests__/generated-context-grammar.test.js +85 -0
- package/dist/shared/generated-context.d.ts +68 -36
- package/dist/shared/generated-context.js +300 -174
- package/dist/shared/tool-groups.js +3 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/postinstall.mjs +9 -0
|
@@ -1,27 +1,17 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Runtime-card grammar for crouter-authored context messages.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// guards, recovery prompts, and node inbox digests are user messages, while
|
|
6
|
-
// bearings, situational updates, and held nudges are custom messages. Attached
|
|
7
|
-
// views should still present all of them as generated context rather than as
|
|
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.
|
|
11
|
-
import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../core/runtime/session-visibility.js';
|
|
12
|
-
export { REVIEW_BOUNDARY_CUSTOM_TYPE };
|
|
3
|
+
// Every runtime message carries one whole-message envelope. customType only
|
|
4
|
+
// controls delivery and visibility; parsing stays independent of runtime and clients.
|
|
13
5
|
/** Custom message carrying the node's session-start bearings. */
|
|
14
6
|
export const CONTEXT_INTRO_CUSTOM_TYPE = 'crtr-context';
|
|
15
7
|
/** Custom message carrying an ambient situational-context update. */
|
|
16
8
|
export const SITUATIONAL_CONTEXT_CUSTOM_TYPE = 'crtr-situational-context';
|
|
17
9
|
/** Custom message used when a context-size nudge waits for the next turn. */
|
|
18
10
|
export const CONTEXT_NUDGE_CUSTOM_TYPE = 'crtr-context-nudge';
|
|
19
|
-
/**
|
|
11
|
+
/** Custom message that opens a review companion's visible transcript. */
|
|
12
|
+
export const REVIEW_BOUNDARY_CUSTOM_TYPE = 'crtr-review-boundary';
|
|
13
|
+
/** Opening text of a pre-envelope fresh-revive kickoff user message. */
|
|
20
14
|
export const REVIVE_KICKOFF_SENTINEL = 'You have been revived fresh after a context refresh';
|
|
21
|
-
/** Prefix shared by the producer and the compatibility classifier for context nudges. */
|
|
22
|
-
export const CONTEXT_NUDGE_PREFIX = '[crtr] Context ~';
|
|
23
|
-
/** Runtime-authored continuation after a cleanly aborted turn survives a restart. */
|
|
24
|
-
export const RUNTIME_RESTART_CONTINUATION = '<runtime-restart-continuation>\ncontinue\n</runtime-restart-continuation>';
|
|
25
15
|
/** Generic completion mandate issued by the terminal-node stop guard. */
|
|
26
16
|
export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
|
|
27
17
|
"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.";
|
|
@@ -29,50 +19,166 @@ export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone a
|
|
|
29
19
|
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.';
|
|
30
20
|
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.';
|
|
31
21
|
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.';
|
|
32
|
-
const PERSONA_TRANSITION_OPEN = '<persona-transition>';
|
|
33
|
-
const PERSONA_TRANSITION_CLOSE = '</persona-transition>';
|
|
34
22
|
const REVIEW_APPROVAL_OPEN = '<crtr-review-approval>';
|
|
35
23
|
const REVIEW_APPROVAL_CLOSE = '</crtr-review-approval>';
|
|
36
24
|
const MODEL_FALLBACK_RECOVERY_OPEN = '<model-fallback-recovery>';
|
|
37
25
|
const MODEL_FALLBACK_RECOVERY_CLOSE = '</model-fallback-recovery>';
|
|
38
26
|
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';
|
|
39
27
|
const STRUCTURED_OUTPUT_REPROMPT_SUFFIX = '\n```';
|
|
40
|
-
/** Mark an active persona transition without changing the guidance inside it. */
|
|
41
|
-
export function formatPersonaTransition(guidance) {
|
|
42
|
-
return `${PERSONA_TRANSITION_OPEN}\n${guidance}\n${PERSONA_TRANSITION_CLOSE}`;
|
|
43
|
-
}
|
|
44
|
-
/** Mark a daemon-delivered review approval for compact viewer presentation. */
|
|
45
|
-
export function formatReviewApproval(body) {
|
|
46
|
-
return `${REVIEW_APPROVAL_OPEN}\n${body}\n${REVIEW_APPROVAL_CLOSE}`;
|
|
47
|
-
}
|
|
48
28
|
/** Format the stop guard's dynamic structured-output mandate. */
|
|
49
29
|
export function formatStructuredOutputReprompt(schema) {
|
|
50
30
|
return `${STRUCTURED_OUTPUT_REPROMPT_PREFIX}${schema}${STRUCTURED_OUTPUT_REPROMPT_SUFFIX}`;
|
|
51
31
|
}
|
|
52
|
-
/**
|
|
32
|
+
/** Keep model-fallback guidance editable without making it a reader contract. */
|
|
53
33
|
export function formatModelFallbackRecovery(previousModel, nextModel, reason) {
|
|
54
|
-
|
|
34
|
+
return reason === 'credential'
|
|
55
35
|
? `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.`
|
|
56
36
|
: `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.`;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
37
|
+
}
|
|
38
|
+
function countFact(facts, key) {
|
|
39
|
+
const value = facts[key];
|
|
40
|
+
if (value === undefined || !/^(?:0|[1-9]\d*)$/.test(value))
|
|
41
|
+
return null;
|
|
42
|
+
return Number(value);
|
|
43
|
+
}
|
|
44
|
+
function inboxSummary(facts) {
|
|
45
|
+
const updates = countFact(facts, 'updates');
|
|
46
|
+
const senders = countFact(facts, 'senders');
|
|
47
|
+
if (updates === null)
|
|
48
|
+
return 'inbox update';
|
|
49
|
+
if (senders === null || senders <= 1)
|
|
50
|
+
return `${updates === 1 ? 'message' : `${updates} messages`} received`;
|
|
51
|
+
return `${updates} messages received from ${senders} nodes`;
|
|
52
|
+
}
|
|
53
|
+
function recoverySummaryFromFacts(facts) {
|
|
54
|
+
switch (facts.reason) {
|
|
55
|
+
case 'model-fallback': return 'model route changed';
|
|
56
|
+
case 'connection': return 'network connection restored';
|
|
57
|
+
case 'provider': return 'provider retry';
|
|
58
|
+
case 'auth': return 'provider credentials updated';
|
|
59
|
+
default: return 'runtime recovery';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function reviewCommentSummary(facts) {
|
|
63
|
+
switch (facts.verb) {
|
|
64
|
+
case 'create': return 'review comment created';
|
|
65
|
+
case 'edit': return 'review comment edited';
|
|
66
|
+
case 'resolve': return 'review comment resolved';
|
|
67
|
+
case 'reopen': return 'review comment reopened';
|
|
68
|
+
case 'delete': return 'review comment deleted';
|
|
69
|
+
default: return 'review comment updated';
|
|
71
70
|
}
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
}
|
|
72
|
+
/** The closed crouter vocabulary, plus custom-role aliases for the same cards. */
|
|
73
|
+
export const KIND_TABLE = {
|
|
74
|
+
inbox: { label: 'crtr inbox', summary: inboxSummary, expandable: true },
|
|
75
|
+
revive: { label: 'crtr revive', summary: () => 'fresh context kickoff', expandable: true },
|
|
76
|
+
'restart-continuation': { label: 'crouter continuation', summary: () => 'continuing from where it left off', expandable: true },
|
|
77
|
+
'stop-guard': {
|
|
78
|
+
label: 'crtr stop guard',
|
|
79
|
+
summary: (facts) => facts.reason === 'structured-output' ? 'structured output required' : 'completion required',
|
|
80
|
+
expandable: true,
|
|
81
|
+
},
|
|
82
|
+
recovery: { label: 'crouter continuation', summary: recoverySummaryFromFacts, expandable: true },
|
|
83
|
+
'persona-transition': { label: 'crtr persona', summary: () => 'runtime role update', expandable: true },
|
|
84
|
+
'review-approval': { label: 'crtr review', summary: () => 'review approved by the user', expandable: true },
|
|
85
|
+
'review-comment': { label: 'crtr review', summary: reviewCommentSummary, expandable: true },
|
|
86
|
+
'review-queued': { label: 'crtr review', summary: () => 'review submitted; completion pending', expandable: true },
|
|
87
|
+
'context-nudge': {
|
|
88
|
+
label: 'crtr',
|
|
89
|
+
summary: (facts) => facts.size === undefined ? 'context-window guidance' : `Context ${facts.size}`,
|
|
90
|
+
expandable: true,
|
|
91
|
+
customTypes: { [CONTEXT_NUDGE_CUSTOM_TYPE]: {} },
|
|
92
|
+
},
|
|
93
|
+
bearings: {
|
|
94
|
+
label: 'crtr context',
|
|
95
|
+
summary: () => 'orienting bearings',
|
|
96
|
+
expandable: true,
|
|
97
|
+
customTypes: { [CONTEXT_INTRO_CUSTOM_TYPE]: {} },
|
|
98
|
+
},
|
|
99
|
+
'review-boundary': {
|
|
100
|
+
label: 'review boundary',
|
|
101
|
+
summary: () => 'earlier conversation is not shown',
|
|
102
|
+
expandable: true,
|
|
103
|
+
customTypes: { [REVIEW_BOUNDARY_CUSTOM_TYPE]: {} },
|
|
104
|
+
},
|
|
105
|
+
situational: {
|
|
106
|
+
label: 'situational context',
|
|
107
|
+
summary: () => 'ambient context update',
|
|
108
|
+
expandable: true,
|
|
109
|
+
customTypes: { [SITUATIONAL_CONTEXT_CUSTOM_TYPE]: {} },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const GENERIC_CARD = {
|
|
113
|
+
label: 'crtr runtime',
|
|
114
|
+
summary: () => 'runtime context',
|
|
115
|
+
expandable: true,
|
|
116
|
+
};
|
|
117
|
+
/** Kinds contributed by a product or plugin, in whatever process renders them. */
|
|
118
|
+
const KIND_REGISTRY = {};
|
|
119
|
+
/** Contribute presentation for namespaced kinds this process will render.
|
|
120
|
+
* Idempotent and last-write-wins, so a re-imported module cannot fail a boot.
|
|
121
|
+
* A process that never registers is not degraded: an unregistered kind still
|
|
122
|
+
* parses and only falls back to the generic label and summary. */
|
|
123
|
+
export function registerCardKinds(kinds) {
|
|
124
|
+
for (const [kind, definition] of Object.entries(kinds)) {
|
|
125
|
+
// Bare kinds are crouter core's closed vocabulary. A product claiming one
|
|
126
|
+
// is a programming error at boot, not a runtime condition to tolerate.
|
|
127
|
+
if (!kind.includes(':'))
|
|
128
|
+
throw new Error(`Runtime card kind must be namespaced (contain ':'): ${kind}`);
|
|
129
|
+
KIND_REGISTRY[kind] = definition;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const XML_ATTRIBUTE_NAME = /^[A-Za-z_:][A-Za-z0-9_:.-]*$/;
|
|
133
|
+
const ATTRIBUTE_RE = /\s+([A-Za-z_:][A-Za-z0-9_:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/gy;
|
|
134
|
+
function escapeXmlAttribute(value) {
|
|
135
|
+
return value
|
|
136
|
+
.replaceAll('&', '&')
|
|
137
|
+
.replaceAll('<', '<')
|
|
138
|
+
.replaceAll('>', '>')
|
|
139
|
+
.replaceAll('"', '"')
|
|
140
|
+
.replaceAll("'", ''');
|
|
141
|
+
}
|
|
142
|
+
function escapeXmlText(value) {
|
|
143
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
144
|
+
}
|
|
145
|
+
function unescapeXmlAttribute(value) {
|
|
146
|
+
return value.replace(/&(?:amp|lt|gt|quot|apos);/g, (entity) => ({
|
|
147
|
+
'&': '&',
|
|
148
|
+
'<': '<',
|
|
149
|
+
'>': '>',
|
|
150
|
+
'"': '"',
|
|
151
|
+
''': "'",
|
|
152
|
+
})[entity]);
|
|
153
|
+
}
|
|
154
|
+
function parseAttributes(source) {
|
|
155
|
+
const attributes = {};
|
|
156
|
+
ATTRIBUTE_RE.lastIndex = 0;
|
|
157
|
+
let index = 0;
|
|
158
|
+
while (index < source.length) {
|
|
159
|
+
if (/^\s*$/.test(source.slice(index)))
|
|
160
|
+
break;
|
|
161
|
+
ATTRIBUTE_RE.lastIndex = index;
|
|
162
|
+
const match = ATTRIBUTE_RE.exec(source);
|
|
163
|
+
if (match === null)
|
|
164
|
+
return null;
|
|
165
|
+
const [, key, doubleQuoted, singleQuoted] = match;
|
|
166
|
+
if (key === undefined || Object.hasOwn(attributes, key))
|
|
167
|
+
return null;
|
|
168
|
+
attributes[key] = unescapeXmlAttribute(doubleQuoted ?? singleQuoted ?? '');
|
|
169
|
+
index = ATTRIBUTE_RE.lastIndex;
|
|
74
170
|
}
|
|
75
|
-
return
|
|
171
|
+
return attributes;
|
|
172
|
+
}
|
|
173
|
+
function formatAttributes(attributes) {
|
|
174
|
+
return Object.entries(attributes)
|
|
175
|
+
.filter(([, value]) => value !== undefined)
|
|
176
|
+
.map(([key, value]) => {
|
|
177
|
+
if (!XML_ATTRIBUTE_NAME.test(key))
|
|
178
|
+
throw new Error(`Invalid XML attribute name: ${key}`);
|
|
179
|
+
return ` ${key}="${escapeXmlAttribute(String(value))}"`;
|
|
180
|
+
})
|
|
181
|
+
.join('');
|
|
76
182
|
}
|
|
77
183
|
/** Plain text from a generated message's string or text-block content. */
|
|
78
184
|
export function generatedContextText(message) {
|
|
@@ -81,62 +187,148 @@ export function generatedContextText(message) {
|
|
|
81
187
|
if (!Array.isArray(message.content))
|
|
82
188
|
return '';
|
|
83
189
|
return message.content
|
|
84
|
-
.filter((block) => typeof block === 'object'
|
|
85
|
-
block !== null
|
|
86
|
-
block.type === 'text'
|
|
87
|
-
typeof block.text === 'string')
|
|
190
|
+
.filter((block) => typeof block === 'object'
|
|
191
|
+
&& block !== null
|
|
192
|
+
&& block.type === 'text'
|
|
193
|
+
&& typeof block.text === 'string')
|
|
88
194
|
.map((block) => block.text)
|
|
89
195
|
.join('');
|
|
90
196
|
}
|
|
91
|
-
/**
|
|
92
|
-
export function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
export function isInboxDigest(text) {
|
|
109
|
-
return INBOX_HEADER_RE.test(text);
|
|
110
|
-
}
|
|
111
|
-
export function extractInboxSender(text) {
|
|
112
|
-
return INBOX_HEADER_RE.exec(text)?.[1] ?? null;
|
|
113
|
-
}
|
|
114
|
-
/** Node ids whose section of this digest reports a finished node. Sections are
|
|
115
|
-
* delimited by their `From <sender>` headers, so a final never attaches to the
|
|
116
|
-
* neighbouring sender. */
|
|
117
|
-
export function inboxFinishedSenders(text) {
|
|
118
|
-
const headers = [...text.matchAll(INBOX_HEADERS_RE)];
|
|
197
|
+
/** Wrap a user-role runtime message in its whole-message card envelope. */
|
|
198
|
+
export function formatCard(kind, facts, body) {
|
|
199
|
+
if (kind === '')
|
|
200
|
+
throw new Error('Runtime card kind is required');
|
|
201
|
+
if (Object.hasOwn(facts, 'kind'))
|
|
202
|
+
throw new Error('Runtime card facts cannot replace kind');
|
|
203
|
+
return `<runtime${formatAttributes({ kind, ...facts })}>${body}</runtime>`;
|
|
204
|
+
}
|
|
205
|
+
/** Wrap a card whose body is DATA, not markup — the only escape on the API
|
|
206
|
+
* path, so a caller can neither emit a malformed card nor smuggle markup into
|
|
207
|
+
* one. `formatCard` keeps its raw body for the trusted crouter producers that
|
|
208
|
+
* deliberately nest markup (bearings blocks, the inbox `<from>`/`<entry>`
|
|
209
|
+
* grammar `parseInboxBody` depends on). */
|
|
210
|
+
export function formatDataCard(kind, facts, body) {
|
|
211
|
+
return formatCard(kind, facts, escapeXmlText(body));
|
|
212
|
+
}
|
|
213
|
+
function parseInboxBody(body) {
|
|
119
214
|
const senders = [];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
215
|
+
const fromRe = /<from\b([^>]*)>([\s\S]*?)<\/from>/g;
|
|
216
|
+
for (const fromMatch of body.matchAll(fromRe)) {
|
|
217
|
+
const attributes = parseAttributes(fromMatch[1] ?? '');
|
|
218
|
+
if (attributes?.id === undefined || attributes.updates === undefined)
|
|
219
|
+
continue;
|
|
220
|
+
const updates = Number(attributes.updates);
|
|
221
|
+
if (!Number.isFinite(updates))
|
|
222
|
+
continue;
|
|
223
|
+
const entries = [];
|
|
224
|
+
const entryBody = fromMatch[2] ?? '';
|
|
225
|
+
const entryRe = /<entry\b([^>]*)>([\s\S]*?)<\/entry>/g;
|
|
226
|
+
for (const entryMatch of entryBody.matchAll(entryRe)) {
|
|
227
|
+
const entryAttributes = parseAttributes(entryMatch[1] ?? '');
|
|
228
|
+
if (entryAttributes?.kind === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
const disposition = entryAttributes.disposition === 'human-answer' || entryAttributes.disposition === 'human-canceled'
|
|
231
|
+
? entryAttributes.disposition
|
|
232
|
+
: 'report';
|
|
233
|
+
entries.push({
|
|
234
|
+
kind: entryAttributes.kind,
|
|
235
|
+
disposition,
|
|
236
|
+
body: unescapeXmlAttribute(entryMatch[2] ?? ''),
|
|
237
|
+
...(entryAttributes.ref === undefined ? {} : { ref: entryAttributes.ref }),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
senders.push({
|
|
241
|
+
id: attributes.id,
|
|
242
|
+
...(attributes.name === undefined ? {} : { name: attributes.name }),
|
|
243
|
+
updates,
|
|
244
|
+
finished: attributes.finished === 'true' || entries.some((entry) => entry.kind === 'final'),
|
|
245
|
+
entries,
|
|
246
|
+
});
|
|
126
247
|
}
|
|
127
248
|
return senders;
|
|
128
249
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
250
|
+
function cardFor(kind, facts, body, senders) {
|
|
251
|
+
const definition = KIND_TABLE[kind] ?? KIND_REGISTRY[kind] ?? GENERIC_CARD;
|
|
252
|
+
return {
|
|
253
|
+
kind,
|
|
254
|
+
facts,
|
|
255
|
+
body,
|
|
256
|
+
senders,
|
|
257
|
+
label: definition.label,
|
|
258
|
+
summary: definition.summary(facts),
|
|
259
|
+
expandable: definition.expandable,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** Parse a whole-message runtime envelope, regardless of delivery role. */
|
|
263
|
+
function parseEnvelope(text) {
|
|
264
|
+
const opening = /^<runtime\b([^>]*)>/.exec(text);
|
|
265
|
+
if (opening === null || !text.endsWith('</runtime>'))
|
|
266
|
+
return null;
|
|
267
|
+
const attributes = parseAttributes(opening[1] ?? '');
|
|
268
|
+
if (attributes?.kind === undefined)
|
|
269
|
+
return null;
|
|
270
|
+
const raw = text.slice(opening[0].length, -'</runtime>'.length);
|
|
271
|
+
const { kind, ...factValues } = attributes;
|
|
272
|
+
const facts = Object.freeze(factValues);
|
|
273
|
+
// A namespaced kind can only have been produced through the API path, whose
|
|
274
|
+
// renderer is `formatDataCard`, so its body is escaped by construction and
|
|
275
|
+
// decodes here. A bare kind's body is raw markup written by a core producer.
|
|
276
|
+
const body = kind.includes(':') ? unescapeXmlAttribute(raw) : raw;
|
|
277
|
+
return cardFor(kind, facts, body, kind === 'inbox' ? parseInboxBody(body) : []);
|
|
278
|
+
}
|
|
279
|
+
/** Parse a runtime envelope, with pre-envelope customType compatibility. */
|
|
280
|
+
export function parseCard(message) {
|
|
281
|
+
const text = generatedContextText(message);
|
|
282
|
+
const envelope = parseEnvelope(text);
|
|
283
|
+
if (envelope !== null)
|
|
284
|
+
return envelope;
|
|
285
|
+
if (message.role !== 'custom')
|
|
286
|
+
return null;
|
|
287
|
+
// Pre-envelope custom-message fallback for old sessions. New custom messages
|
|
288
|
+
// classify through their envelope; customType is delivery metadata only.
|
|
289
|
+
for (const [kind, definition] of Object.entries(KIND_TABLE)) {
|
|
290
|
+
const override = message.customType === undefined ? undefined : definition.customTypes?.[message.customType];
|
|
291
|
+
if (override === undefined)
|
|
292
|
+
continue;
|
|
293
|
+
const facts = Object.freeze({});
|
|
294
|
+
return {
|
|
295
|
+
kind,
|
|
296
|
+
facts,
|
|
297
|
+
body: text,
|
|
298
|
+
senders: [],
|
|
299
|
+
label: override.label ?? definition.label,
|
|
300
|
+
summary: (override.summary ?? definition.summary)(facts),
|
|
301
|
+
expandable: override.expandable ?? definition.expandable,
|
|
302
|
+
};
|
|
137
303
|
}
|
|
138
|
-
return
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
/** Format a complete inbox card from report bodies resolved by the caller. */
|
|
307
|
+
export function formatInboxCard(sections) {
|
|
308
|
+
const body = sections.map((section) => {
|
|
309
|
+
const updates = section.updates ?? section.entries.length;
|
|
310
|
+
const finished = section.finished ?? section.entries.some((entry) => entry.kind === 'final');
|
|
311
|
+
const entries = section.entries.map((entry) => {
|
|
312
|
+
const disposition = entry.disposition === 'human-answer' || entry.disposition === 'human-canceled'
|
|
313
|
+
? entry.disposition
|
|
314
|
+
: undefined;
|
|
315
|
+
return `<entry${formatAttributes({ kind: entry.kind, ref: entry.ref, disposition })}>${escapeXmlText(entry.body)}</entry>`;
|
|
316
|
+
}).join('\n');
|
|
317
|
+
return `<from${formatAttributes({ id: section.id, name: section.name, updates, finished: finished || undefined })}>${entries}</from>`;
|
|
318
|
+
}).join('\n');
|
|
319
|
+
const updates = sections.reduce((total, section) => total + (section.updates ?? section.entries.length), 0);
|
|
320
|
+
return formatCard('inbox', { senders: sections.length, updates }, body);
|
|
139
321
|
}
|
|
322
|
+
// LEGACY — landed 2026-08-14. Pre-envelope runtime text, kept ONLY so old
|
|
323
|
+
// scrollback is not attributed to the person. Delete this section and its
|
|
324
|
+
// callers one week after that date; after deletion, pre-cut scrollback
|
|
325
|
+
// misattributes and that cost is accepted.
|
|
326
|
+
const LEGACY_RESTART_CONTINUATION = '<runtime-restart-continuation>\ncontinue\n</runtime-restart-continuation>';
|
|
327
|
+
const CONTEXT_NUDGE_PREFIX = '[crtr] Context ~';
|
|
328
|
+
const PERSONA_TRANSITION_OPEN = '<persona-transition>';
|
|
329
|
+
const PERSONA_TRANSITION_CLOSE = '</persona-transition>';
|
|
330
|
+
const INBOX_SENDER_ID = '[a-z0-9]+(?:-[a-z0-9]+)+';
|
|
331
|
+
const INBOX_HEADER_RE = new RegExp(`^From (?:(.+?) \\[(${INBOX_SENDER_ID})\\]|(${INBOX_SENDER_ID}|system|human|crtrd)) — (\\d+) update`, 'm');
|
|
140
332
|
function isPersonaTransition(body) {
|
|
141
333
|
return body.startsWith(`${PERSONA_TRANSITION_OPEN}\n`) && body.endsWith(`\n${PERSONA_TRANSITION_CLOSE}`);
|
|
142
334
|
}
|
|
@@ -159,92 +351,26 @@ function isStructuredOutputReprompt(body) {
|
|
|
159
351
|
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\.$/;
|
|
160
352
|
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\.$/;
|
|
161
353
|
function isModelFallbackRecovery(body) {
|
|
162
|
-
if (!body.startsWith(`${MODEL_FALLBACK_RECOVERY_OPEN}\n`)
|
|
163
|
-
|| !body.endsWith(`\n${MODEL_FALLBACK_RECOVERY_CLOSE}`)) {
|
|
354
|
+
if (!body.startsWith(`${MODEL_FALLBACK_RECOVERY_OPEN}\n`) || !body.endsWith(`\n${MODEL_FALLBACK_RECOVERY_CLOSE}`)) {
|
|
164
355
|
return false;
|
|
165
356
|
}
|
|
166
357
|
const guidance = body.slice(MODEL_FALLBACK_RECOVERY_OPEN.length + 1, -(MODEL_FALLBACK_RECOVERY_CLOSE.length + 1));
|
|
167
358
|
return MODEL_CREDENTIAL_RECOVERY_RE.test(guidance) || MODEL_NOT_FOUND_RECOVERY_RE.test(guidance);
|
|
168
359
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (body === AUTH_FAULT_RECOVERY_BODY)
|
|
176
|
-
return 'provider credentials updated';
|
|
177
|
-
if (body === CONNECTION_FAULT_RECOVERY_BODY)
|
|
178
|
-
return 'network connection restored';
|
|
179
|
-
if (body === PROVIDER_FAULT_RECOVERY_BODY)
|
|
180
|
-
return 'provider retry';
|
|
181
|
-
if (body === RUNTIME_RESTART_CONTINUATION)
|
|
182
|
-
return 'continuing from where it left off';
|
|
183
|
-
if (isModelFallbackRecovery(body))
|
|
184
|
-
return 'model route changed';
|
|
185
|
-
return null;
|
|
360
|
+
function isRecoveryBody(body) {
|
|
361
|
+
return body === AUTH_FAULT_RECOVERY_BODY
|
|
362
|
+
|| body === CONNECTION_FAULT_RECOVERY_BODY
|
|
363
|
+
|| body === PROVIDER_FAULT_RECOVERY_BODY
|
|
364
|
+
|| body === LEGACY_RESTART_CONTINUATION
|
|
365
|
+
|| isModelFallbackRecovery(body);
|
|
186
366
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (message.customType === REVIEW_BOUNDARY_CUSTOM_TYPE) {
|
|
197
|
-
return { kind: 'bearings', label: 'review boundary', summary: 'earlier conversation is not shown', body };
|
|
198
|
-
}
|
|
199
|
-
if (message.customType === CONTEXT_INTRO_CUSTOM_TYPE) {
|
|
200
|
-
return { kind: 'bearings', label: 'crtr context', summary: 'orienting bearings', body };
|
|
201
|
-
}
|
|
202
|
-
if (message.customType === SITUATIONAL_CONTEXT_CUSTOM_TYPE) {
|
|
203
|
-
return {
|
|
204
|
-
kind: 'situational',
|
|
205
|
-
label: 'situational context',
|
|
206
|
-
summary: 'ambient context update',
|
|
207
|
-
body,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
if (message.customType === CONTEXT_NUDGE_CUSTOM_TYPE) {
|
|
211
|
-
return { kind: 'context-nudge', label: 'crtr', summary: nudgeSummary(body), body };
|
|
212
|
-
}
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
if (message.role !== 'user')
|
|
216
|
-
return null;
|
|
217
|
-
if (body.startsWith(REVIVE_KICKOFF_SENTINEL)) {
|
|
218
|
-
return { kind: 'revive', label: 'crtr revive', summary: 'fresh context kickoff', body };
|
|
219
|
-
}
|
|
220
|
-
if (body.startsWith(CONTEXT_NUDGE_PREFIX)) {
|
|
221
|
-
return { kind: 'context-nudge', label: 'crtr', summary: nudgeSummary(body), body };
|
|
222
|
-
}
|
|
223
|
-
if (isInboxDigest(body)) {
|
|
224
|
-
return { kind: 'inbox', label: 'crtr inbox', summary: inboxSummary(body), body };
|
|
225
|
-
}
|
|
226
|
-
if (isReviewApproval(body)) {
|
|
227
|
-
return { kind: 'review-approval', label: 'crtr review', summary: 'review approved by the user', body };
|
|
228
|
-
}
|
|
229
|
-
if (isPersonaTransition(body)) {
|
|
230
|
-
return {
|
|
231
|
-
kind: 'persona-transition',
|
|
232
|
-
label: 'crtr persona',
|
|
233
|
-
summary: 'runtime role update',
|
|
234
|
-
body,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
if (body === STALL_REPROMPT || isStructuredOutputReprompt(body)) {
|
|
238
|
-
return {
|
|
239
|
-
kind: 'stop-guard',
|
|
240
|
-
label: 'crtr stop guard',
|
|
241
|
-
summary: body === STALL_REPROMPT ? 'completion required' : 'structured output required',
|
|
242
|
-
body,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
const recovery = recoverySummary(body);
|
|
246
|
-
if (recovery !== null) {
|
|
247
|
-
return { kind: 'recovery', label: 'crouter continuation', summary: recovery, body };
|
|
248
|
-
}
|
|
249
|
-
return null;
|
|
367
|
+
export function isLegacyRuntimeText(text) {
|
|
368
|
+
return text.startsWith(REVIVE_KICKOFF_SENTINEL)
|
|
369
|
+
|| text.startsWith(CONTEXT_NUDGE_PREFIX)
|
|
370
|
+
|| INBOX_HEADER_RE.test(text)
|
|
371
|
+
|| isReviewApproval(text)
|
|
372
|
+
|| isPersonaTransition(text)
|
|
373
|
+
|| text === STALL_REPROMPT
|
|
374
|
+
|| isStructuredOutputReprompt(text)
|
|
375
|
+
|| isRecoveryBody(text);
|
|
250
376
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Shared transcript-boundary rules for broker-owned tool-group summaries.
|
|
2
2
|
// Keep this independent of renderer state: both live event tracking and snapshot
|
|
3
3
|
// reconstruction must agree on which prose closes a run of tools.
|
|
4
|
-
import {
|
|
4
|
+
import { generatedContextText, isLegacyRuntimeText, parseCard } from './generated-context.js';
|
|
5
5
|
const TOOL_GROUP_SUMMARY_KEYS = new Set(['bullets', 'filesDeleted']);
|
|
6
6
|
const MAX_TOOL_GROUP_BULLETS = 5;
|
|
7
7
|
const MAX_TOOL_GROUP_BULLET_LENGTH = 1_000;
|
|
@@ -53,5 +53,6 @@ export function assistantVisibleText(message) {
|
|
|
53
53
|
/** A person-authored user message closes the preceding group. Runtime-generated
|
|
54
54
|
* user context retains role=user for delivery semantics but is not human prose. */
|
|
55
55
|
export function isTrueUserMessage(message) {
|
|
56
|
-
|
|
56
|
+
const text = generatedContextText(message);
|
|
57
|
+
return message.role === 'user' && parseCard(message) === null && !isLegacyRuntimeText(text);
|
|
57
58
|
}
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.206",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.206",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { patchPiTreeRootOrdering } from './patch-pi-tree-root-ordering.mjs';
|
|
@@ -34,6 +35,14 @@ async function main() {
|
|
|
34
35
|
console.warn('[crtr postinstall] pi tree-root-ordering patch failed (non-fatal):', error?.message ?? error);
|
|
35
36
|
}
|
|
36
37
|
if (process.env.CI) return;
|
|
38
|
+
// A published tarball always ships `dist/`; a source checkout being installed
|
|
39
|
+
// never does, and its runtime generation comes from an explicit
|
|
40
|
+
// `npm run build && npm run install-runtime` afterwards. Staging an unbuilt
|
|
41
|
+
// tree can only fail its smoke validation.
|
|
42
|
+
if (!existsSync(join(ROOT, 'dist', 'index.js'))) {
|
|
43
|
+
console.log('crouter source checkout installed. Run `npm run build && npm run install-runtime` to produce a runtime generation.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
37
46
|
try {
|
|
38
47
|
await installRuntime();
|
|
39
48
|
} catch (error) {
|