@north-light/crouter-api 0.3.203 → 0.3.205
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
CHANGED
package/dist/api/dto/nodes.d.ts
CHANGED
|
@@ -5,12 +5,13 @@ export interface CreateNodeRequest {
|
|
|
5
5
|
prompt?: string;
|
|
6
6
|
profile?: string;
|
|
7
7
|
mode?: ModeDTO;
|
|
8
|
-
/** The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
/** The directory the create came from. It is what the daemon selects a
|
|
9
|
+
* profile from when the request neither names one nor inherits one from a
|
|
10
|
+
* parent, and — for a node with no `pin_cwd` and no spawner to inherit from —
|
|
11
|
+
* where the node lands. */
|
|
11
12
|
cwd?: string;
|
|
12
|
-
/** Pin the node to this exact directory, overriding the
|
|
13
|
-
*
|
|
13
|
+
/** Pin the node to this exact directory, overriding both the spawner's
|
|
14
|
+
* directory and the launch cwd. See `crtr node new --cwd`. */
|
|
14
15
|
pin_cwd?: string;
|
|
15
16
|
/** Display name (tmux window + resume picker). Defaults to the kind. */
|
|
16
17
|
name?: string;
|
|
@@ -187,6 +188,8 @@ export interface NodeMessagesPageDTO {
|
|
|
187
188
|
node_id: NodeIdDTO;
|
|
188
189
|
/** pi AgentMessage[] JSON, chronological ascending within this page. */
|
|
189
190
|
messages: unknown[];
|
|
191
|
+
/** Stable session-entry ids aligned 1:1 with `messages`. */
|
|
192
|
+
message_ids?: string[];
|
|
190
193
|
/** Opaque cursor toward older messages; null at the start of the session. */
|
|
191
194
|
next_cursor: Cursor | null;
|
|
192
195
|
captured_at: IsoTime;
|
package/dist/api/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { CrtrClientOptions } from './client.js';
|
|
|
3
3
|
export { ApiError, isErrorBody } from './errors.js';
|
|
4
4
|
export type { ErrorBody } from './errors.js';
|
|
5
5
|
export { API_VERSION, routes } from './routes.js';
|
|
6
|
+
export * from '../shared/generated-context.js';
|
|
6
7
|
export * from './dto/common.js';
|
|
7
8
|
export * from './dto/health.js';
|
|
8
9
|
export * from './dto/nodes.js';
|
package/dist/api/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export { CrtrClient } from './client.js';
|
|
5
5
|
export { ApiError, isErrorBody } from './errors.js';
|
|
6
6
|
export { API_VERSION, routes } from './routes.js';
|
|
7
|
+
export * from '../shared/generated-context.js';
|
|
7
8
|
export * from './dto/common.js';
|
|
8
9
|
export * from './dto/health.js';
|
|
9
10
|
export * from './dto/nodes.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Custom message carrying the node's session-start bearings. */
|
|
2
|
+
export declare const CONTEXT_INTRO_CUSTOM_TYPE = "crtr-context";
|
|
3
|
+
/** Custom message carrying an ambient situational-context update. */
|
|
4
|
+
export declare const SITUATIONAL_CONTEXT_CUSTOM_TYPE = "crtr-situational-context";
|
|
5
|
+
/** Custom message used when a context-size nudge waits for the next turn. */
|
|
6
|
+
export declare const CONTEXT_NUDGE_CUSTOM_TYPE = "crtr-context-nudge";
|
|
7
|
+
/** Custom message that opens a review companion's visible transcript. */
|
|
8
|
+
export declare const REVIEW_BOUNDARY_CUSTOM_TYPE = "crtr-review-boundary";
|
|
9
|
+
/** Opening text of a pre-envelope fresh-revive kickoff user message. */
|
|
10
|
+
export declare const REVIVE_KICKOFF_SENTINEL = "You have been revived fresh after a context refresh";
|
|
11
|
+
/** Generic completion mandate issued by the terminal-node stop guard. */
|
|
12
|
+
export declare const STALL_REPROMPT: string;
|
|
13
|
+
/** Static recovery prompts shared by the broker producer and display classifier. */
|
|
14
|
+
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.";
|
|
15
|
+
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.";
|
|
16
|
+
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.";
|
|
17
|
+
export type ModelFallbackRecoveryReason = 'credential' | 'not-found';
|
|
18
|
+
/** Format the stop guard's dynamic structured-output mandate. */
|
|
19
|
+
export declare function formatStructuredOutputReprompt(schema: string): string;
|
|
20
|
+
/** Keep model-fallback guidance editable without making it a reader contract. */
|
|
21
|
+
export declare function formatModelFallbackRecovery(previousModel: string, nextModel: string, reason: ModelFallbackRecoveryReason): string;
|
|
22
|
+
export interface GeneratedContextMessageLike {
|
|
23
|
+
role?: string;
|
|
24
|
+
customType?: string;
|
|
25
|
+
content?: unknown;
|
|
26
|
+
}
|
|
27
|
+
export type CardEntryDisposition = 'report' | 'human-answer' | 'human-canceled';
|
|
28
|
+
export interface CardEntry {
|
|
29
|
+
disposition: CardEntryDisposition;
|
|
30
|
+
kind: string;
|
|
31
|
+
body: string;
|
|
32
|
+
ref?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CardSender {
|
|
35
|
+
id: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
updates: number;
|
|
38
|
+
finished: boolean;
|
|
39
|
+
entries: CardEntry[];
|
|
40
|
+
}
|
|
41
|
+
/** An already-resolved inbox entry supplied to the pure digest formatter. */
|
|
42
|
+
export interface InboxCardEntry {
|
|
43
|
+
kind: string;
|
|
44
|
+
body: string;
|
|
45
|
+
ref?: string;
|
|
46
|
+
disposition?: CardEntryDisposition;
|
|
47
|
+
}
|
|
48
|
+
/** An already-resolved sender section supplied to the pure digest formatter. */
|
|
49
|
+
export interface InboxCardSection {
|
|
50
|
+
id: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
entries: readonly InboxCardEntry[];
|
|
53
|
+
updates?: number;
|
|
54
|
+
finished?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface GeneratedCard {
|
|
57
|
+
kind: string;
|
|
58
|
+
facts: Readonly<Record<string, string>>;
|
|
59
|
+
body: string;
|
|
60
|
+
senders: CardSender[];
|
|
61
|
+
label: string;
|
|
62
|
+
summary: string;
|
|
63
|
+
expandable: boolean;
|
|
64
|
+
}
|
|
65
|
+
type CardSummary = (facts: Readonly<Record<string, string>>) => string;
|
|
66
|
+
export interface CardKindDefinition {
|
|
67
|
+
label: string;
|
|
68
|
+
summary: CardSummary;
|
|
69
|
+
expandable: boolean;
|
|
70
|
+
customTypes?: Readonly<Record<string, Partial<Pick<CardKindDefinition, 'label' | 'summary' | 'expandable'>>>>;
|
|
71
|
+
}
|
|
72
|
+
/** The closed crouter vocabulary, plus custom-role aliases for the same cards. */
|
|
73
|
+
export declare const KIND_TABLE: Readonly<Record<string, CardKindDefinition>>;
|
|
74
|
+
/** Plain text from a generated message's string or text-block content. */
|
|
75
|
+
export declare function generatedContextText(message: GeneratedContextMessageLike): string;
|
|
76
|
+
/** Wrap a user-role runtime message in its whole-message card envelope. */
|
|
77
|
+
export declare function formatCard(kind: string, facts: Record<string, string | number | boolean | undefined>, body: string): string;
|
|
78
|
+
/** Parse a runtime envelope, with pre-envelope customType compatibility. */
|
|
79
|
+
export declare function parseCard(message: GeneratedContextMessageLike): GeneratedCard | null;
|
|
80
|
+
/** Format a complete inbox card from report bodies resolved by the caller. */
|
|
81
|
+
export declare function formatInboxCard(sections: readonly InboxCardSection[]): string;
|
|
82
|
+
export declare function isLegacyRuntimeText(text: string): boolean;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Runtime-card grammar for crouter-authored context messages.
|
|
2
|
+
//
|
|
3
|
+
// Every runtime message carries one whole-message envelope. customType only
|
|
4
|
+
// controls delivery and visibility; parsing stays independent of runtime and clients.
|
|
5
|
+
/** Custom message carrying the node's session-start bearings. */
|
|
6
|
+
export const CONTEXT_INTRO_CUSTOM_TYPE = 'crtr-context';
|
|
7
|
+
/** Custom message carrying an ambient situational-context update. */
|
|
8
|
+
export const SITUATIONAL_CONTEXT_CUSTOM_TYPE = 'crtr-situational-context';
|
|
9
|
+
/** Custom message used when a context-size nudge waits for the next turn. */
|
|
10
|
+
export const CONTEXT_NUDGE_CUSTOM_TYPE = 'crtr-context-nudge';
|
|
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. */
|
|
14
|
+
export const REVIVE_KICKOFF_SENTINEL = 'You have been revived fresh after a context refresh';
|
|
15
|
+
/** Generic completion mandate issued by the terminal-node stop guard. */
|
|
16
|
+
export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
|
|
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.";
|
|
18
|
+
/** Static recovery prompts shared by the broker producer and display classifier. */
|
|
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.';
|
|
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.';
|
|
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.';
|
|
22
|
+
const REVIEW_APPROVAL_OPEN = '<crtr-review-approval>';
|
|
23
|
+
const REVIEW_APPROVAL_CLOSE = '</crtr-review-approval>';
|
|
24
|
+
const MODEL_FALLBACK_RECOVERY_OPEN = '<model-fallback-recovery>';
|
|
25
|
+
const MODEL_FALLBACK_RECOVERY_CLOSE = '</model-fallback-recovery>';
|
|
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';
|
|
27
|
+
const STRUCTURED_OUTPUT_REPROMPT_SUFFIX = '\n```';
|
|
28
|
+
/** Format the stop guard's dynamic structured-output mandate. */
|
|
29
|
+
export function formatStructuredOutputReprompt(schema) {
|
|
30
|
+
return `${STRUCTURED_OUTPUT_REPROMPT_PREFIX}${schema}${STRUCTURED_OUTPUT_REPROMPT_SUFFIX}`;
|
|
31
|
+
}
|
|
32
|
+
/** Keep model-fallback guidance editable without making it a reader contract. */
|
|
33
|
+
export function formatModelFallbackRecovery(previousModel, nextModel, reason) {
|
|
34
|
+
return reason === 'credential'
|
|
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.`
|
|
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.`;
|
|
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';
|
|
70
|
+
}
|
|
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
|
+
const XML_ATTRIBUTE_NAME = /^[A-Za-z_:][A-Za-z0-9_:.-]*$/;
|
|
118
|
+
const ATTRIBUTE_RE = /\s+([A-Za-z_:][A-Za-z0-9_:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/gy;
|
|
119
|
+
function escapeXmlAttribute(value) {
|
|
120
|
+
return value
|
|
121
|
+
.replaceAll('&', '&')
|
|
122
|
+
.replaceAll('<', '<')
|
|
123
|
+
.replaceAll('>', '>')
|
|
124
|
+
.replaceAll('"', '"')
|
|
125
|
+
.replaceAll("'", ''');
|
|
126
|
+
}
|
|
127
|
+
function escapeXmlText(value) {
|
|
128
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
129
|
+
}
|
|
130
|
+
function unescapeXmlAttribute(value) {
|
|
131
|
+
return value.replace(/&(?:amp|lt|gt|quot|apos);/g, (entity) => ({
|
|
132
|
+
'&': '&',
|
|
133
|
+
'<': '<',
|
|
134
|
+
'>': '>',
|
|
135
|
+
'"': '"',
|
|
136
|
+
''': "'",
|
|
137
|
+
})[entity]);
|
|
138
|
+
}
|
|
139
|
+
function parseAttributes(source) {
|
|
140
|
+
const attributes = {};
|
|
141
|
+
ATTRIBUTE_RE.lastIndex = 0;
|
|
142
|
+
let index = 0;
|
|
143
|
+
while (index < source.length) {
|
|
144
|
+
if (/^\s*$/.test(source.slice(index)))
|
|
145
|
+
break;
|
|
146
|
+
ATTRIBUTE_RE.lastIndex = index;
|
|
147
|
+
const match = ATTRIBUTE_RE.exec(source);
|
|
148
|
+
if (match === null)
|
|
149
|
+
return null;
|
|
150
|
+
const [, key, doubleQuoted, singleQuoted] = match;
|
|
151
|
+
if (key === undefined || Object.hasOwn(attributes, key))
|
|
152
|
+
return null;
|
|
153
|
+
attributes[key] = unescapeXmlAttribute(doubleQuoted ?? singleQuoted ?? '');
|
|
154
|
+
index = ATTRIBUTE_RE.lastIndex;
|
|
155
|
+
}
|
|
156
|
+
return attributes;
|
|
157
|
+
}
|
|
158
|
+
function formatAttributes(attributes) {
|
|
159
|
+
return Object.entries(attributes)
|
|
160
|
+
.filter(([, value]) => value !== undefined)
|
|
161
|
+
.map(([key, value]) => {
|
|
162
|
+
if (!XML_ATTRIBUTE_NAME.test(key))
|
|
163
|
+
throw new Error(`Invalid XML attribute name: ${key}`);
|
|
164
|
+
return ` ${key}="${escapeXmlAttribute(String(value))}"`;
|
|
165
|
+
})
|
|
166
|
+
.join('');
|
|
167
|
+
}
|
|
168
|
+
/** Plain text from a generated message's string or text-block content. */
|
|
169
|
+
export function generatedContextText(message) {
|
|
170
|
+
if (typeof message.content === 'string')
|
|
171
|
+
return message.content;
|
|
172
|
+
if (!Array.isArray(message.content))
|
|
173
|
+
return '';
|
|
174
|
+
return message.content
|
|
175
|
+
.filter((block) => typeof block === 'object'
|
|
176
|
+
&& block !== null
|
|
177
|
+
&& block.type === 'text'
|
|
178
|
+
&& typeof block.text === 'string')
|
|
179
|
+
.map((block) => block.text)
|
|
180
|
+
.join('');
|
|
181
|
+
}
|
|
182
|
+
/** Wrap a user-role runtime message in its whole-message card envelope. */
|
|
183
|
+
export function formatCard(kind, facts, body) {
|
|
184
|
+
if (kind === '')
|
|
185
|
+
throw new Error('Runtime card kind is required');
|
|
186
|
+
if (Object.hasOwn(facts, 'kind'))
|
|
187
|
+
throw new Error('Runtime card facts cannot replace kind');
|
|
188
|
+
return `<runtime${formatAttributes({ kind, ...facts })}>${body}</runtime>`;
|
|
189
|
+
}
|
|
190
|
+
function parseInboxBody(body) {
|
|
191
|
+
const senders = [];
|
|
192
|
+
const fromRe = /<from\b([^>]*)>([\s\S]*?)<\/from>/g;
|
|
193
|
+
for (const fromMatch of body.matchAll(fromRe)) {
|
|
194
|
+
const attributes = parseAttributes(fromMatch[1] ?? '');
|
|
195
|
+
if (attributes?.id === undefined || attributes.updates === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
const updates = Number(attributes.updates);
|
|
198
|
+
if (!Number.isFinite(updates))
|
|
199
|
+
continue;
|
|
200
|
+
const entries = [];
|
|
201
|
+
const entryBody = fromMatch[2] ?? '';
|
|
202
|
+
const entryRe = /<entry\b([^>]*)>([\s\S]*?)<\/entry>/g;
|
|
203
|
+
for (const entryMatch of entryBody.matchAll(entryRe)) {
|
|
204
|
+
const entryAttributes = parseAttributes(entryMatch[1] ?? '');
|
|
205
|
+
if (entryAttributes?.kind === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
const disposition = entryAttributes.disposition === 'human-answer' || entryAttributes.disposition === 'human-canceled'
|
|
208
|
+
? entryAttributes.disposition
|
|
209
|
+
: 'report';
|
|
210
|
+
entries.push({
|
|
211
|
+
kind: entryAttributes.kind,
|
|
212
|
+
disposition,
|
|
213
|
+
body: unescapeXmlAttribute(entryMatch[2] ?? ''),
|
|
214
|
+
...(entryAttributes.ref === undefined ? {} : { ref: entryAttributes.ref }),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
senders.push({
|
|
218
|
+
id: attributes.id,
|
|
219
|
+
...(attributes.name === undefined ? {} : { name: attributes.name }),
|
|
220
|
+
updates,
|
|
221
|
+
finished: attributes.finished === 'true' || entries.some((entry) => entry.kind === 'final'),
|
|
222
|
+
entries,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return senders;
|
|
226
|
+
}
|
|
227
|
+
function cardFor(kind, facts, body, senders) {
|
|
228
|
+
const definition = KIND_TABLE[kind] ?? GENERIC_CARD;
|
|
229
|
+
return {
|
|
230
|
+
kind,
|
|
231
|
+
facts,
|
|
232
|
+
body,
|
|
233
|
+
senders,
|
|
234
|
+
label: definition.label,
|
|
235
|
+
summary: definition.summary(facts),
|
|
236
|
+
expandable: definition.expandable,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Parse a whole-message runtime envelope, regardless of delivery role. */
|
|
240
|
+
function parseEnvelope(text) {
|
|
241
|
+
const opening = /^<runtime\b([^>]*)>/.exec(text);
|
|
242
|
+
if (opening === null || !text.endsWith('</runtime>'))
|
|
243
|
+
return null;
|
|
244
|
+
const attributes = parseAttributes(opening[1] ?? '');
|
|
245
|
+
if (attributes?.kind === undefined)
|
|
246
|
+
return null;
|
|
247
|
+
const body = text.slice(opening[0].length, -'</runtime>'.length);
|
|
248
|
+
const { kind, ...factValues } = attributes;
|
|
249
|
+
const facts = Object.freeze(factValues);
|
|
250
|
+
return cardFor(kind, facts, body, kind === 'inbox' ? parseInboxBody(body) : []);
|
|
251
|
+
}
|
|
252
|
+
/** Parse a runtime envelope, with pre-envelope customType compatibility. */
|
|
253
|
+
export function parseCard(message) {
|
|
254
|
+
const text = generatedContextText(message);
|
|
255
|
+
const envelope = parseEnvelope(text);
|
|
256
|
+
if (envelope !== null)
|
|
257
|
+
return envelope;
|
|
258
|
+
if (message.role !== 'custom')
|
|
259
|
+
return null;
|
|
260
|
+
// Pre-envelope custom-message fallback for old sessions. New custom messages
|
|
261
|
+
// classify through their envelope; customType is delivery metadata only.
|
|
262
|
+
for (const [kind, definition] of Object.entries(KIND_TABLE)) {
|
|
263
|
+
const override = message.customType === undefined ? undefined : definition.customTypes?.[message.customType];
|
|
264
|
+
if (override === undefined)
|
|
265
|
+
continue;
|
|
266
|
+
const facts = Object.freeze({});
|
|
267
|
+
return {
|
|
268
|
+
kind,
|
|
269
|
+
facts,
|
|
270
|
+
body: text,
|
|
271
|
+
senders: [],
|
|
272
|
+
label: override.label ?? definition.label,
|
|
273
|
+
summary: (override.summary ?? definition.summary)(facts),
|
|
274
|
+
expandable: override.expandable ?? definition.expandable,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
/** Format a complete inbox card from report bodies resolved by the caller. */
|
|
280
|
+
export function formatInboxCard(sections) {
|
|
281
|
+
const body = sections.map((section) => {
|
|
282
|
+
const updates = section.updates ?? section.entries.length;
|
|
283
|
+
const finished = section.finished ?? section.entries.some((entry) => entry.kind === 'final');
|
|
284
|
+
const entries = section.entries.map((entry) => {
|
|
285
|
+
const disposition = entry.disposition === 'human-answer' || entry.disposition === 'human-canceled'
|
|
286
|
+
? entry.disposition
|
|
287
|
+
: undefined;
|
|
288
|
+
return `<entry${formatAttributes({ kind: entry.kind, ref: entry.ref, disposition })}>${escapeXmlText(entry.body)}</entry>`;
|
|
289
|
+
}).join('\n');
|
|
290
|
+
return `<from${formatAttributes({ id: section.id, name: section.name, updates, finished: finished || undefined })}>${entries}</from>`;
|
|
291
|
+
}).join('\n');
|
|
292
|
+
const updates = sections.reduce((total, section) => total + (section.updates ?? section.entries.length), 0);
|
|
293
|
+
return formatCard('inbox', { senders: sections.length, updates }, body);
|
|
294
|
+
}
|
|
295
|
+
// LEGACY — landed 2026-08-14. Pre-envelope runtime text, kept ONLY so old
|
|
296
|
+
// scrollback is not attributed to the person. Delete this section and its
|
|
297
|
+
// callers one week after that date; after deletion, pre-cut scrollback
|
|
298
|
+
// misattributes and that cost is accepted.
|
|
299
|
+
const LEGACY_RESTART_CONTINUATION = '<runtime-restart-continuation>\ncontinue\n</runtime-restart-continuation>';
|
|
300
|
+
const CONTEXT_NUDGE_PREFIX = '[crtr] Context ~';
|
|
301
|
+
const PERSONA_TRANSITION_OPEN = '<persona-transition>';
|
|
302
|
+
const PERSONA_TRANSITION_CLOSE = '</persona-transition>';
|
|
303
|
+
const INBOX_SENDER_ID = '[a-z0-9]+(?:-[a-z0-9]+)+';
|
|
304
|
+
const INBOX_HEADER_RE = new RegExp(`^From (?:(.+?) \\[(${INBOX_SENDER_ID})\\]|(${INBOX_SENDER_ID}|system|human|crtrd)) — (\\d+) update`, 'm');
|
|
305
|
+
function isPersonaTransition(body) {
|
|
306
|
+
return body.startsWith(`${PERSONA_TRANSITION_OPEN}\n`) && body.endsWith(`\n${PERSONA_TRANSITION_CLOSE}`);
|
|
307
|
+
}
|
|
308
|
+
function isReviewApproval(body) {
|
|
309
|
+
return body.startsWith(`${REVIEW_APPROVAL_OPEN}\n`) && body.endsWith(`\n${REVIEW_APPROVAL_CLOSE}`);
|
|
310
|
+
}
|
|
311
|
+
function isStructuredOutputReprompt(body) {
|
|
312
|
+
if (!body.startsWith(STRUCTURED_OUTPUT_REPROMPT_PREFIX) || !body.endsWith(STRUCTURED_OUTPUT_REPROMPT_SUFFIX)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const schema = body.slice(STRUCTURED_OUTPUT_REPROMPT_PREFIX.length, -STRUCTURED_OUTPUT_REPROMPT_SUFFIX.length);
|
|
316
|
+
try {
|
|
317
|
+
JSON.parse(schema);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
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\.$/;
|
|
325
|
+
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\.$/;
|
|
326
|
+
function isModelFallbackRecovery(body) {
|
|
327
|
+
if (!body.startsWith(`${MODEL_FALLBACK_RECOVERY_OPEN}\n`) || !body.endsWith(`\n${MODEL_FALLBACK_RECOVERY_CLOSE}`)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const guidance = body.slice(MODEL_FALLBACK_RECOVERY_OPEN.length + 1, -(MODEL_FALLBACK_RECOVERY_CLOSE.length + 1));
|
|
331
|
+
return MODEL_CREDENTIAL_RECOVERY_RE.test(guidance) || MODEL_NOT_FOUND_RECOVERY_RE.test(guidance);
|
|
332
|
+
}
|
|
333
|
+
function isRecoveryBody(body) {
|
|
334
|
+
return body === AUTH_FAULT_RECOVERY_BODY
|
|
335
|
+
|| body === CONNECTION_FAULT_RECOVERY_BODY
|
|
336
|
+
|| body === PROVIDER_FAULT_RECOVERY_BODY
|
|
337
|
+
|| body === LEGACY_RESTART_CONTINUATION
|
|
338
|
+
|| isModelFallbackRecovery(body);
|
|
339
|
+
}
|
|
340
|
+
export function isLegacyRuntimeText(text) {
|
|
341
|
+
return text.startsWith(REVIVE_KICKOFF_SENTINEL)
|
|
342
|
+
|| text.startsWith(CONTEXT_NUDGE_PREFIX)
|
|
343
|
+
|| INBOX_HEADER_RE.test(text)
|
|
344
|
+
|| isReviewApproval(text)
|
|
345
|
+
|| isPersonaTransition(text)
|
|
346
|
+
|| text === STALL_REPROMPT
|
|
347
|
+
|| isStructuredOutputReprompt(text)
|
|
348
|
+
|| isRecoveryBody(text);
|
|
349
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter-api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.205",
|
|
4
4
|
"description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, and the CrtrClient. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/api/index.js",
|