@north-light/crouter 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 +2 -0
- 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 +88 -21
- package/dist/core/__tests__/human-deliver.test.js +66 -5
- 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/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 +4 -2
- package/dist/core/runtime/broker/inbox.d.ts +14 -2
- package/dist/core/runtime/broker/inbox.js +52 -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-protocol.d.ts +9 -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/kickoff.d.ts +3 -4
- package/dist/core/runtime/kickoff.js +23 -13
- 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-live.js +2 -1
- 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/nodes.js +10 -7
- package/dist/daemon/companion-retire.d.ts +2 -2
- package/dist/daemon/companion-retire.js +16 -9
- package/dist/daemon/human/finish.d.ts +2 -2
- package/dist/daemon/human/finish.js +48 -8
- package/dist/daemon/human/sweep.js +3 -2
- 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 +3 -2
- 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 +4 -3
- package/dist/pi-extensions/canvas-goal-capture.js +10 -6
- package/dist/pi-extensions/canvas-inbox-watcher.js +36 -25
- 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 +57 -36
- package/dist/shared/generated-context.js +273 -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,62 +1,83 @@
|
|
|
1
|
-
import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../core/runtime/session-visibility.js';
|
|
2
|
-
export { REVIEW_BOUNDARY_CUSTOM_TYPE };
|
|
3
1
|
/** Custom message carrying the node's session-start bearings. */
|
|
4
2
|
export declare const CONTEXT_INTRO_CUSTOM_TYPE = "crtr-context";
|
|
5
3
|
/** Custom message carrying an ambient situational-context update. */
|
|
6
4
|
export declare const SITUATIONAL_CONTEXT_CUSTOM_TYPE = "crtr-situational-context";
|
|
7
5
|
/** Custom message used when a context-size nudge waits for the next turn. */
|
|
8
6
|
export declare const CONTEXT_NUDGE_CUSTOM_TYPE = "crtr-context-nudge";
|
|
9
|
-
/**
|
|
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
10
|
export declare const REVIVE_KICKOFF_SENTINEL = "You have been revived fresh after a context refresh";
|
|
11
|
-
/** Prefix shared by the producer and the compatibility classifier for context nudges. */
|
|
12
|
-
export declare const CONTEXT_NUDGE_PREFIX = "[crtr] Context ~";
|
|
13
|
-
/** Runtime-authored continuation after a cleanly aborted turn survives a restart. */
|
|
14
|
-
export declare const RUNTIME_RESTART_CONTINUATION = "<runtime-restart-continuation>\ncontinue\n</runtime-restart-continuation>";
|
|
15
11
|
/** Generic completion mandate issued by the terminal-node stop guard. */
|
|
16
12
|
export declare const STALL_REPROMPT: string;
|
|
17
13
|
/** Static recovery prompts shared by the broker producer and display classifier. */
|
|
18
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.";
|
|
19
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.";
|
|
20
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.";
|
|
21
|
-
|
|
22
|
-
export declare function formatPersonaTransition(guidance: string): string;
|
|
23
|
-
/** Mark a daemon-delivered review approval for compact viewer presentation. */
|
|
24
|
-
export declare function formatReviewApproval(body: string): string;
|
|
17
|
+
export type ModelFallbackRecoveryReason = 'credential' | 'not-found';
|
|
25
18
|
/** Format the stop guard's dynamic structured-output mandate. */
|
|
26
19
|
export declare function formatStructuredOutputReprompt(schema: string): string;
|
|
27
|
-
|
|
28
|
-
/** Mark a model-route fallback re-drive while retaining its full guidance. */
|
|
20
|
+
/** Keep model-fallback guidance editable without making it a reader contract. */
|
|
29
21
|
export declare function formatModelFallbackRecovery(previousModel: string, nextModel: string, reason: ModelFallbackRecoveryReason): string;
|
|
30
22
|
export interface GeneratedContextMessageLike {
|
|
31
23
|
role?: string;
|
|
32
24
|
customType?: string;
|
|
33
25
|
content?: unknown;
|
|
34
26
|
}
|
|
35
|
-
export
|
|
36
|
-
|
|
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[];
|
|
37
61
|
label: string;
|
|
38
62
|
summary: string;
|
|
39
|
-
|
|
40
|
-
|
|
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'>>>>;
|
|
41
71
|
}
|
|
42
|
-
|
|
72
|
+
/** The closed crouter vocabulary, plus custom-role aliases for the same cards. */
|
|
73
|
+
export declare const KIND_TABLE: Readonly<Record<string, CardKindDefinition>>;
|
|
43
74
|
/** Plain text from a generated message's string or text-block content. */
|
|
44
75
|
export declare function generatedContextText(message: GeneratedContextMessageLike): string;
|
|
45
|
-
/**
|
|
46
|
-
export declare function
|
|
47
|
-
|
|
48
|
-
export declare function
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
export
|
|
53
|
-
/** Exact producer-owned marker for the continuation a successor broker inserts
|
|
54
|
-
* after a runtime restart. Other recovery bodies intentionally do not match. */
|
|
55
|
-
export declare function isRuntimeRestartContinuation(message: GeneratedContextMessageLike): boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Classify a crouter-generated context message for display. The checks are
|
|
58
|
-
* exact producer-owned contracts: custom messages use their customType, and
|
|
59
|
-
* user messages use stable sentinels or the same exact formatter/constants as
|
|
60
|
-
* their producers. Ordinary user prompts remain ordinary user prompts.
|
|
61
|
-
*/
|
|
62
|
-
export declare function generatedContextPresentation(message: GeneratedContextMessageLike): GeneratedContextPresentation | null;
|
|
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 {};
|
|
@@ -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,151 @@ 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';
|
|
71
60
|
}
|
|
72
|
-
|
|
73
|
-
|
|
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';
|
|
74
70
|
}
|
|
75
|
-
|
|
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('');
|
|
76
167
|
}
|
|
77
168
|
/** Plain text from a generated message's string or text-block content. */
|
|
78
169
|
export function generatedContextText(message) {
|
|
@@ -81,62 +172,136 @@ export function generatedContextText(message) {
|
|
|
81
172
|
if (!Array.isArray(message.content))
|
|
82
173
|
return '';
|
|
83
174
|
return message.content
|
|
84
|
-
.filter((block) => typeof block === 'object'
|
|
85
|
-
block !== null
|
|
86
|
-
block.type === 'text'
|
|
87
|
-
typeof block.text === 'string')
|
|
175
|
+
.filter((block) => typeof block === 'object'
|
|
176
|
+
&& block !== null
|
|
177
|
+
&& block.type === 'text'
|
|
178
|
+
&& typeof block.text === 'string')
|
|
88
179
|
.map((block) => block.text)
|
|
89
180
|
.join('');
|
|
90
181
|
}
|
|
91
|
-
/**
|
|
92
|
-
export function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// messages are deliberately delivered verbatim and never carry this header.
|
|
101
|
-
// Senders are node ids, the two reserved words, or the daemon itself (crtrd —
|
|
102
|
-
// review notes and other daemon-originated node messages).
|
|
103
|
-
const INBOX_HEADER_RE = /^From ([a-z0-9]+(?:-[a-z0-9]+)+|system|human|crtrd) — (\d+) update/m;
|
|
104
|
-
const INBOX_HEADERS_RE = /^From ([a-z0-9]+(?:-[a-z0-9]+)+|system|human|crtrd) — (\d+) update/gm;
|
|
105
|
-
// Entry lines inside a section, rendered by core/runtime/broker/inbox.ts as
|
|
106
|
-
// ` [<kind>]`. A final is the one kind that ends the sender's node.
|
|
107
|
-
const FINISHED_ENTRY_RE = /^ {2}\[(?:final|completed)\]/m;
|
|
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)];
|
|
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) {
|
|
119
191
|
const senders = [];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
});
|
|
126
224
|
}
|
|
127
225
|
return senders;
|
|
128
226
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
};
|
|
137
276
|
}
|
|
138
|
-
return
|
|
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);
|
|
139
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');
|
|
140
305
|
function isPersonaTransition(body) {
|
|
141
306
|
return body.startsWith(`${PERSONA_TRANSITION_OPEN}\n`) && body.endsWith(`\n${PERSONA_TRANSITION_CLOSE}`);
|
|
142
307
|
}
|
|
@@ -159,92 +324,26 @@ function isStructuredOutputReprompt(body) {
|
|
|
159
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\.$/;
|
|
160
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\.$/;
|
|
161
326
|
function isModelFallbackRecovery(body) {
|
|
162
|
-
if (!body.startsWith(`${MODEL_FALLBACK_RECOVERY_OPEN}\n`)
|
|
163
|
-
|| !body.endsWith(`\n${MODEL_FALLBACK_RECOVERY_CLOSE}`)) {
|
|
327
|
+
if (!body.startsWith(`${MODEL_FALLBACK_RECOVERY_OPEN}\n`) || !body.endsWith(`\n${MODEL_FALLBACK_RECOVERY_CLOSE}`)) {
|
|
164
328
|
return false;
|
|
165
329
|
}
|
|
166
330
|
const guidance = body.slice(MODEL_FALLBACK_RECOVERY_OPEN.length + 1, -(MODEL_FALLBACK_RECOVERY_CLOSE.length + 1));
|
|
167
331
|
return MODEL_CREDENTIAL_RECOVERY_RE.test(guidance) || MODEL_NOT_FOUND_RECOVERY_RE.test(guidance);
|
|
168
332
|
}
|
|
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;
|
|
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);
|
|
186
339
|
}
|
|
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;
|
|
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);
|
|
250
349
|
}
|
|
@@ -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
|
}
|