@north-light/crouter 0.3.185 → 0.3.187
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 +0 -32
- package/dist/api/dto/messages.d.ts +0 -5
- package/dist/builtin-memory/internal/agent-shaping.md +1 -1
- package/dist/builtin-memory/internal/plugins.md +9 -0
- package/dist/clients/attach/input/controller.js +3 -4
- package/dist/clients/attach/overlays/mcp.js +1 -3
- package/dist/clients/attach/slash/dispatch.js +0 -2
- package/dist/clients/attach/viewer.js +331 -331
- package/dist/commands/memory/list.js +7 -3
- package/dist/commands/pkg/plugin-manage.js +39 -5
- package/dist/core/__tests__/plugin-kinds.test.d.ts +1 -0
- package/dist/core/__tests__/plugin-kinds.test.js +167 -0
- package/dist/core/__tests__/session-model.test.js +2 -4
- package/dist/core/command-plugins/bundle.d.ts +6 -0
- package/dist/core/command-plugins/bundle.js +29 -7
- package/dist/core/config.d.ts +12 -1
- package/dist/core/config.js +84 -12
- package/dist/core/installed-plugins.d.ts +2 -0
- package/dist/core/installed-plugins.js +42 -0
- package/dist/core/resolver.d.ts +1 -1
- package/dist/core/resolver.js +7 -32
- package/dist/core/runtime/broker/event-projection.d.ts +1 -8
- package/dist/core/runtime/broker/event-projection.js +1 -25
- package/dist/core/runtime/broker/frame-dispatch.d.ts +0 -2
- package/dist/core/runtime/broker/frame-dispatch.js +8 -41
- package/dist/core/runtime/broker-protocol.d.ts +1 -46
- package/dist/core/runtime/broker.js +1 -8
- package/dist/core/runtime/interactive-deliver.d.ts +2 -5
- package/dist/core/runtime/interactive-deliver.js +3 -6
- package/dist/core/session-model/session-state.d.ts +3 -6
- package/dist/core/session-model/session-state.js +1 -5
- package/dist/daemon/api/handlers/messages.js +1 -7
- package/dist/types.d.ts +12 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/runtime/broker/message-ledger.d.ts +0 -53
- package/dist/core/runtime/broker/message-ledger.js +0 -143
package/dist/core/resolver.js
CHANGED
|
@@ -1,39 +1,14 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { CONFIG_FILE } from '../types.js';
|
|
3
2
|
import { readConfig } from './config.js';
|
|
4
|
-
import { listDirs, pathExists
|
|
5
|
-
import { readMarketplaceManifest
|
|
3
|
+
import { listDirs, pathExists } from './fs-utils.js';
|
|
4
|
+
import { readMarketplaceManifest } from './manifest.js';
|
|
6
5
|
import { InputError } from './io.js';
|
|
7
6
|
import { marketplacesDir, projectScopeRoot, scopeRoot, userScopeRoot, } from './scope.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const dir = join(scopeRootPath, 'plugins');
|
|
14
|
-
if (!pathExists(dir))
|
|
15
|
-
return [];
|
|
16
|
-
const cfg = pluginConfigForRoot(scopeRootPath);
|
|
17
|
-
const out = [];
|
|
18
|
-
for (const name of listDirs(dir)) {
|
|
19
|
-
const root = join(dir, name);
|
|
20
|
-
const manifest = readPluginManifest(root);
|
|
21
|
-
if (!manifest)
|
|
22
|
-
continue;
|
|
23
|
-
const entry = cfg[name];
|
|
24
|
-
const version = typeof entry?.version === 'string' ? entry.version : manifest.version;
|
|
25
|
-
out.push({
|
|
26
|
-
name,
|
|
27
|
-
scope,
|
|
28
|
-
root,
|
|
29
|
-
manifest,
|
|
30
|
-
enabled: typeof entry?.enabled === 'boolean' ? entry.enabled : true,
|
|
31
|
-
sourceMarketplace: typeof entry?.source_marketplace === 'string' ? entry.source_marketplace : undefined,
|
|
32
|
-
version,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
7
|
+
// Moved to installed-plugins.ts (a leaf module below config.ts) so
|
|
8
|
+
// `readMergedLaunchConfig` can layer plugin-declared kinds without an import
|
|
9
|
+
// cycle; re-exported here for this module's existing callers.
|
|
10
|
+
export { listInstalledPluginsInRoot } from './installed-plugins.js';
|
|
11
|
+
import { listInstalledPluginsInRoot } from './installed-plugins.js';
|
|
37
12
|
export function listInstalledPlugins(scope) {
|
|
38
13
|
// The builtin scope has no scopeRoot, so this returns [] — builtin content is
|
|
39
14
|
// the memory substrate, not plugins.
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { BrokerToClient } from '../broker-protocol.js';
|
|
2
2
|
import type { BrokerSession } from './read-ops.js';
|
|
3
3
|
import type { BrokerClientRegistry } from './client-registry.js';
|
|
4
|
-
import type { MessageIdLedger } from './message-ledger.js';
|
|
5
4
|
import type { SubscriptionGeneration } from './rebind.js';
|
|
6
5
|
import type { FaultRetry } from './fault-retry.js';
|
|
7
6
|
type EventProjectionDeps = {
|
|
8
7
|
registry: BrokerClientRegistry;
|
|
9
8
|
faultRetry: FaultRetry;
|
|
10
|
-
ledger: MessageIdLedger;
|
|
11
9
|
installedGeneration: () => SubscriptionGeneration | undefined;
|
|
12
10
|
currentSession: () => BrokerSession;
|
|
13
11
|
notifyTurnAccepted: () => void;
|
|
@@ -33,12 +31,7 @@ export declare class EventProjection {
|
|
|
33
31
|
reset(): void;
|
|
34
32
|
dispose(): void;
|
|
35
33
|
knownStreamWait(deadlineMs: number): () => void;
|
|
36
|
-
|
|
37
|
-
* into the ledger and relay id arrays parallel to the texts; attach the
|
|
38
|
-
* retired/dispatched id to the user message that carries it. Both are
|
|
39
|
-
* additive fields on the verbatim pi event — consumers read or ignore them. */
|
|
40
|
-
private withMessageIdentity;
|
|
41
|
-
relayEvent(rawEvent: BrokerToClient, generation: SubscriptionGeneration): void;
|
|
34
|
+
relayEvent(event: BrokerToClient, generation: SubscriptionGeneration): void;
|
|
42
35
|
private clearPendingUpdate;
|
|
43
36
|
private flushPendingUpdate;
|
|
44
37
|
private invalidatePendingBroadcasts;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { contentText } from '@earendil-works/pi-ai';
|
|
2
1
|
import { readConfig } from '../../config.js';
|
|
3
2
|
import { emitEvent } from '../../events/emit.js';
|
|
4
3
|
import { releaseKnownStreamWait, resolveStreamWatchdogMs, StreamWatchdog } from '../stream-watchdog.js';
|
|
@@ -65,30 +64,7 @@ export class EventProjection {
|
|
|
65
64
|
releaseKnownStreamWait(this.streamWatchdog, generation.id, waitingSession.isStreaming, generation.executingTools.size > 0);
|
|
66
65
|
};
|
|
67
66
|
}
|
|
68
|
-
|
|
69
|
-
* into the ledger and relay id arrays parallel to the texts; attach the
|
|
70
|
-
* retired/dispatched id to the user message that carries it. Both are
|
|
71
|
-
* additive fields on the verbatim pi event — consumers read or ignore them. */
|
|
72
|
-
withMessageIdentity(event) {
|
|
73
|
-
if (event.type === 'queue_update') {
|
|
74
|
-
const ids = this.deps.ledger.reconcileQueueUpdate(event.steering, event.followUp);
|
|
75
|
-
const enriched = { ...event, steeringIds: ids.steeringIds, followUpIds: ids.followUpIds };
|
|
76
|
-
return enriched;
|
|
77
|
-
}
|
|
78
|
-
if (event.type === 'message_start' && event.message.role === 'user') {
|
|
79
|
-
// contentText(content, '') is pi's OWN retirement text extraction
|
|
80
|
-
// (agent-session.js _handleAgentEvent) — the same function, so the
|
|
81
|
-
// ledger's text matching cannot diverge from pi's queue splice.
|
|
82
|
-
const crtrMessageId = this.deps.ledger.takeUserMessageId(contentText(event.message.content, ''));
|
|
83
|
-
if (crtrMessageId === undefined)
|
|
84
|
-
return event;
|
|
85
|
-
const enriched = { ...event, crtrMessageId };
|
|
86
|
-
return enriched;
|
|
87
|
-
}
|
|
88
|
-
return event;
|
|
89
|
-
}
|
|
90
|
-
relayEvent(rawEvent, generation) {
|
|
91
|
-
const event = this.withMessageIdentity(rawEvent);
|
|
67
|
+
relayEvent(event, generation) {
|
|
92
68
|
this.deps.faultRetry.onEvent(event);
|
|
93
69
|
const type = event.type;
|
|
94
70
|
if (type === 'message_start' && event.message.role === 'user') {
|
|
@@ -6,7 +6,6 @@ import type { BrokerClient, BrokerClientRegistry } from './client-registry.js';
|
|
|
6
6
|
import { type BrokerSession } from './read-ops.js';
|
|
7
7
|
import type { ToolGroupTracker } from './tool-groups.js';
|
|
8
8
|
import type { EventProjection } from './event-projection.js';
|
|
9
|
-
import type { MessageIdLedger } from './message-ledger.js';
|
|
10
9
|
import type { RebindDriver } from './rebind.js';
|
|
11
10
|
export declare const REVIEW_BOUNDARY_NAVIGATION_CODE = "review_boundary_navigation";
|
|
12
11
|
export declare class ReviewBoundaryNavigationError extends Error {
|
|
@@ -181,7 +180,6 @@ export type FrameDispatchDeps = {
|
|
|
181
180
|
registry: BrokerClientRegistry;
|
|
182
181
|
toolGroups: ToolGroupTracker;
|
|
183
182
|
projection: EventProjection;
|
|
184
|
-
ledger: MessageIdLedger;
|
|
185
183
|
rebind: RebindDriver;
|
|
186
184
|
reviewCompanion: boolean;
|
|
187
185
|
pendingDialogs: Map<string, PendingDialog>;
|
|
@@ -279,7 +279,7 @@ export function stripHiddenEntriesFromJsonlExport(filePath) {
|
|
|
279
279
|
}
|
|
280
280
|
/** Owns frame-local input state while dispatching through explicit broker collaborators. */
|
|
281
281
|
export function createFrameDispatchContext(deps) {
|
|
282
|
-
const { nodeId, cfg, registry, toolGroups, projection,
|
|
282
|
+
const { nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
|
|
283
283
|
const currentSession = () => rebind.session();
|
|
284
284
|
const currentServices = () => rebind.services();
|
|
285
285
|
let kickoffPending;
|
|
@@ -301,14 +301,9 @@ export function createFrameDispatchContext(deps) {
|
|
|
301
301
|
const prompt = kickoffPending;
|
|
302
302
|
kickoffPending = undefined;
|
|
303
303
|
kickoffInFlight = true;
|
|
304
|
-
// The kickoff prompt is a broker-originated dispatch — mint its identity
|
|
305
|
-
// here so its relayed user message_start carries an id like any other.
|
|
306
|
-
const kickoffMessageId = randomUUID();
|
|
307
|
-
ledger.noteDispatch(kickoffMessageId, prompt);
|
|
308
304
|
trackPromptDispatch(operationIdContext.fresh(() => currentSession()
|
|
309
305
|
.prompt(prompt)
|
|
310
306
|
.catch((err) => {
|
|
311
|
-
ledger.dropIntent(kickoffMessageId);
|
|
312
307
|
emitEvent({
|
|
313
308
|
level: 'error',
|
|
314
309
|
event: 'broker.prompt.first_failed',
|
|
@@ -397,7 +392,6 @@ export function createFrameDispatchContext(deps) {
|
|
|
397
392
|
// is producing now, then start this as a new turn. A normal mid-stream
|
|
398
393
|
// prompt deliberately steers into pi's queue; the whip must not.
|
|
399
394
|
if (frame.type === 'prompt' && frame.interrupt === true) {
|
|
400
|
-
const whipMessageId = frame.message_id;
|
|
401
395
|
return (async () => {
|
|
402
396
|
if (currentSession().isBashRunning) {
|
|
403
397
|
currentSession().abortBash();
|
|
@@ -420,7 +414,6 @@ export function createFrameDispatchContext(deps) {
|
|
|
420
414
|
type: 'prompt',
|
|
421
415
|
text: frame.text,
|
|
422
416
|
...(frame.images !== undefined ? { images: frame.images } : {}),
|
|
423
|
-
...(whipMessageId !== undefined ? { message_id: whipMessageId } : {}),
|
|
424
417
|
});
|
|
425
418
|
})().catch((err) => {
|
|
426
419
|
relayError(err);
|
|
@@ -451,19 +444,6 @@ export function createFrameDispatchContext(deps) {
|
|
|
451
444
|
const dispatch = () => {
|
|
452
445
|
const route = resolveEngineRoute(frame, currentSession().isStreaming);
|
|
453
446
|
const guidance = resolveFrameGuidance(frame.text);
|
|
454
|
-
// Message identity (see MessageIdLedger): note the dispatch intent
|
|
455
|
-
// before the engine call so pi's synchronous queue_update/message_start
|
|
456
|
-
// can adopt it. A leading engine command never produces a matching
|
|
457
|
-
// user message — no intent. On rejection the intent is dropped.
|
|
458
|
-
let noteFailure = relayError;
|
|
459
|
-
if (!isLeadingEngineCommand(frame.text, currentSession())) {
|
|
460
|
-
const messageId = frame.message_id ?? randomUUID();
|
|
461
|
-
ledger.noteDispatch(messageId, frame.text);
|
|
462
|
-
noteFailure = (err) => {
|
|
463
|
-
ledger.dropIntent(messageId);
|
|
464
|
-
relayError(err);
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
447
|
if (guidance === null) {
|
|
468
448
|
if (route.call === 'prompt') {
|
|
469
449
|
// A routed prompt STARTS a turn — including an idle follow_up
|
|
@@ -472,12 +452,12 @@ export function createFrameDispatchContext(deps) {
|
|
|
472
452
|
// steer.
|
|
473
453
|
return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
|
|
474
454
|
.prompt(frame.text, route.options)
|
|
475
|
-
.catch(
|
|
455
|
+
.catch(relayError))));
|
|
476
456
|
}
|
|
477
457
|
// C1: pi's followUp() takes images as a POSITIONAL 2nd arg (`followUp(text, images?)`).
|
|
478
458
|
return settle(currentSession()
|
|
479
459
|
.followUp(frame.text, frame.images)
|
|
480
|
-
.catch(
|
|
460
|
+
.catch(relayError));
|
|
481
461
|
}
|
|
482
462
|
const deliverAs = chooseGuidanceDeliveryMode(frame.type, route.call, currentSession().isStreaming);
|
|
483
463
|
if (route.call === 'prompt') {
|
|
@@ -489,18 +469,18 @@ export function createFrameDispatchContext(deps) {
|
|
|
489
469
|
// ATOMICALLY clears its own pending entry if prompt() settles
|
|
490
470
|
// without ever reaching that drain (an early throw/return leaves
|
|
491
471
|
// nothing stranded to leak into a later, unrelated turn).
|
|
492
|
-
return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => currentSession().prompt(frame.text, route.options))).catch(
|
|
472
|
+
return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => currentSession().prompt(frame.text, route.options))).catch(relayError)));
|
|
493
473
|
}
|
|
494
474
|
return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
|
|
495
475
|
.prompt(frame.text, route.options)
|
|
496
|
-
.catch(
|
|
476
|
+
.catch(relayError)
|
|
497
477
|
.then(() => sendGuidance(guidance, deliverAs)))));
|
|
498
478
|
}
|
|
499
479
|
// deliverAs is always 'followUp' here per chooseGuidanceDeliveryMode's
|
|
500
480
|
// contract (a followUp routeCall never yields 'nextTurn').
|
|
501
481
|
return settle(currentSession()
|
|
502
482
|
.followUp(frame.text, frame.images)
|
|
503
|
-
.catch(
|
|
483
|
+
.catch(relayError)
|
|
504
484
|
.then(() => sendGuidance(guidance, deliverAs)));
|
|
505
485
|
};
|
|
506
486
|
// /login acknowledges before its asynchronous local refresh completes.
|
|
@@ -530,8 +510,6 @@ export function createFrameDispatchContext(deps) {
|
|
|
530
510
|
// and executes it immediately even while streaming. It also expands a
|
|
531
511
|
// matching skill/template before applying the same steer behavior.
|
|
532
512
|
if (resolveSteerCall(frame.text, currentSession()) === 'prompt') {
|
|
533
|
-
// The steer→prompt leading-command path never yields a matching user
|
|
534
|
-
// message — no ledger intent (see MessageIdLedger).
|
|
535
513
|
void currentSession()
|
|
536
514
|
.prompt(frame.text, {
|
|
537
515
|
images: frame.images,
|
|
@@ -540,25 +518,17 @@ export function createFrameDispatchContext(deps) {
|
|
|
540
518
|
.catch(relayError);
|
|
541
519
|
break;
|
|
542
520
|
}
|
|
543
|
-
// Message identity: note the intent before steer() so pi's synchronous
|
|
544
|
-
// enqueue queue_update can adopt it; drop it if the call rejects.
|
|
545
|
-
const steerMessageId = frame.message_id ?? randomUUID();
|
|
546
|
-
ledger.noteDispatch(steerMessageId, frame.text);
|
|
547
|
-
const steerFailure = (err) => {
|
|
548
|
-
ledger.dropIntent(steerMessageId);
|
|
549
|
-
relayError(err);
|
|
550
|
-
};
|
|
551
521
|
const guidance = resolveFrameGuidance(frame.text);
|
|
552
522
|
// C1: pi's steer() takes images as a POSITIONAL 2nd arg (`steer(text, images?)`).
|
|
553
523
|
if (guidance === null) {
|
|
554
524
|
void currentSession()
|
|
555
525
|
.steer(frame.text, frame.images)
|
|
556
|
-
.catch(
|
|
526
|
+
.catch(relayError);
|
|
557
527
|
break;
|
|
558
528
|
}
|
|
559
529
|
void currentSession()
|
|
560
530
|
.steer(frame.text, frame.images)
|
|
561
|
-
.catch(
|
|
531
|
+
.catch(relayError)
|
|
562
532
|
.then(() => sendGuidance(guidance, 'steer'));
|
|
563
533
|
break;
|
|
564
534
|
}
|
|
@@ -836,9 +806,6 @@ export function createFrameDispatchContext(deps) {
|
|
|
836
806
|
type: 'prompt',
|
|
837
807
|
text: frame.text,
|
|
838
808
|
...(frame.images !== undefined ? { images: frame.images } : {}),
|
|
839
|
-
// Carry the sender-minted message identity onto the synthesized prompt
|
|
840
|
-
// frame so a gateway/Core deliver retires its optimistic entry by id.
|
|
841
|
-
...(frame.message_id !== undefined ? { message_id: frame.message_id } : {}),
|
|
842
809
|
});
|
|
843
810
|
if (via === 'steer' || outcome === undefined) {
|
|
844
811
|
turnAcceptedWaiters.delete(ackAccepted);
|
|
@@ -78,27 +78,16 @@ export interface PromptFrame {
|
|
|
78
78
|
images?: ImageContent[];
|
|
79
79
|
/** Abort the active turn before starting this prompt. Used by the attach viewer's whip action so its command replaces, rather than joins, in-flight work. */
|
|
80
80
|
interrupt?: true;
|
|
81
|
-
/** Sender-minted message identity (see MessageIdLedger). The broker mirrors
|
|
82
|
-
* it through pi's id-less queues and echoes it back on the relayed user
|
|
83
|
-
* `message_start` (`crtrMessageId`) and enriched `queue_update`
|
|
84
|
-
* (`steeringIds`/`followUpIds`), so an optimistic sender retires its
|
|
85
|
-
* pending entry by exact id instead of text/order guessing. Absent (a
|
|
86
|
-
* legacy client, kickoff, inbox): the broker mints one itself. */
|
|
87
|
-
message_id?: string;
|
|
88
81
|
}
|
|
89
82
|
export interface SteerFrame {
|
|
90
83
|
type: 'steer';
|
|
91
84
|
text: string;
|
|
92
85
|
images?: ImageContent[];
|
|
93
|
-
/** Sender-minted message identity — see {@link PromptFrame.message_id}. */
|
|
94
|
-
message_id?: string;
|
|
95
86
|
}
|
|
96
87
|
export interface FollowUpFrame {
|
|
97
88
|
type: 'follow_up';
|
|
98
89
|
text: string;
|
|
99
90
|
images?: ImageContent[];
|
|
100
|
-
/** Sender-minted message identity — see {@link PromptFrame.message_id}. */
|
|
101
|
-
message_id?: string;
|
|
102
91
|
}
|
|
103
92
|
export interface AbortFrame {
|
|
104
93
|
type: 'abort';
|
|
@@ -122,9 +111,6 @@ export interface DeliverFrame {
|
|
|
122
111
|
id: string;
|
|
123
112
|
text: string;
|
|
124
113
|
images?: ImageContent[];
|
|
125
|
-
/** Sender-minted message identity — see {@link PromptFrame.message_id}.
|
|
126
|
-
* Distinct from `id`, the one-shot ack-correlation token. */
|
|
127
|
-
message_id?: string;
|
|
128
114
|
}
|
|
129
115
|
/** Run a `!` bash command — writable clients only. Maps to `session.executeBash()`,
|
|
130
116
|
* which runs the command, records a `bashExecution` message in context, and
|
|
@@ -365,38 +351,7 @@ export interface WelcomeFrame {
|
|
|
365
351
|
* can construct an AuthStorage/ModelRegistry pointing at the SAME auth.json.
|
|
366
352
|
* crtr surface attach is tmux-local only: broker + viewer share a filesystem. */
|
|
367
353
|
agentDir?: string;
|
|
368
|
-
|
|
369
|
-
* ledger, oldest first — so a reattaching follower (Core's snapshot wake
|
|
370
|
-
* replay) can key the snapshot's trailing user wakes by id instead of
|
|
371
|
-
* discarding them heuristically. Live-process memory only: a broker restart
|
|
372
|
-
* loses it (accepted — optimistic pending sends don't survive that either).
|
|
373
|
-
* Absent on a broker pinned to an older runtime generation. */
|
|
374
|
-
recentUserMessages?: Array<{
|
|
375
|
-
id: string;
|
|
376
|
-
text: string;
|
|
377
|
-
}>;
|
|
378
|
-
}
|
|
379
|
-
/** A relayed pi `queue_update`, enriched by the broker with id arrays PARALLEL
|
|
380
|
-
* to pi's text arrays (`steeringIds[i]` identifies `steering[i]`). The ids are
|
|
381
|
-
* the broker's message ledger mirror of pi's queues — sender-minted when the
|
|
382
|
-
* frame carried `message_id`, broker-minted otherwise. Absent on a broker
|
|
383
|
-
* pinned to an older runtime generation; consumers fall back to bare texts. */
|
|
384
|
-
export type RelayedQueueUpdate = Extract<AgentSessionEvent, {
|
|
385
|
-
type: 'queue_update';
|
|
386
|
-
}> & {
|
|
387
|
-
steeringIds?: string[];
|
|
388
|
-
followUpIds?: string[];
|
|
389
|
-
};
|
|
390
|
-
/** A relayed user `message_start`, enriched by the broker with the message's
|
|
391
|
-
* crouter identity — the id staged by the retirement `queue_update` pi emits
|
|
392
|
-
* synchronously before it, or the matching dispatch intent for an idle
|
|
393
|
-
* prompt. Absent when the message carries none (an engine-command expansion,
|
|
394
|
-
* a pre-ledger replay, an older broker). */
|
|
395
|
-
export type RelayedUserMessageStart = Extract<AgentSessionEvent, {
|
|
396
|
-
type: 'message_start';
|
|
397
|
-
}> & {
|
|
398
|
-
crtrMessageId?: string;
|
|
399
|
-
};
|
|
354
|
+
}
|
|
400
355
|
/** Broadcast to EVERY client after a successful `set_model`/`cycle_model`. pi
|
|
401
356
|
* emits no AgentSessionEvent for a model switch, so without this the new model
|
|
402
357
|
* reaches no viewer at all (the requester gets only a bare ack) and footers
|
|
@@ -44,7 +44,6 @@ import { BrokerClientRegistry } from './broker/client-registry.js';
|
|
|
44
44
|
import { ToolGroupTracker } from './broker/tool-groups.js';
|
|
45
45
|
import { FaultRetry } from './broker/fault-retry.js';
|
|
46
46
|
import { EventProjection } from './broker/event-projection.js';
|
|
47
|
-
import { MessageIdLedger } from './broker/message-ledger.js';
|
|
48
47
|
import { createFrameDispatchContext, handleFrame } from './broker/frame-dispatch.js';
|
|
49
48
|
import { RebindDriver } from './broker/rebind.js';
|
|
50
49
|
import { brokerExtensionState, commitBrokerModel } from './broker/daemon-ops.js';
|
|
@@ -285,9 +284,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
285
284
|
let projection;
|
|
286
285
|
let notifyTurnAccepted = () => { };
|
|
287
286
|
let resetFrameState = () => { };
|
|
288
|
-
// Broker-owned message identity (see MessageIdLedger): mirrors pi's id-less
|
|
289
|
-
// queues so relayed queue_update/user message_start frames carry ids.
|
|
290
|
-
const messageLedger = new MessageIdLedger();
|
|
291
287
|
rebind = new RebindDriver({
|
|
292
288
|
nodeId, cfg, session, services, runtime, registry, toolGroups,
|
|
293
289
|
uiContext: () => uiContext,
|
|
@@ -297,7 +293,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
297
293
|
disposeAndExit: (reason, status) => disposeAndExit(reason, status),
|
|
298
294
|
onSessionRebind: () => {
|
|
299
295
|
resetFrameState();
|
|
300
|
-
messageLedger.reset();
|
|
301
296
|
},
|
|
302
297
|
reWelcomeAll: () => reWelcomeAll(),
|
|
303
298
|
});
|
|
@@ -339,7 +334,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
339
334
|
projection = new EventProjection({
|
|
340
335
|
registry,
|
|
341
336
|
faultRetry,
|
|
342
|
-
ledger: messageLedger,
|
|
343
337
|
installedGeneration: () => rebind.installedGeneration(),
|
|
344
338
|
currentSession: () => rebind.session(),
|
|
345
339
|
notifyTurnAccepted: () => notifyTurnAccepted(),
|
|
@@ -396,7 +390,6 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
396
390
|
role: client.role,
|
|
397
391
|
pending_dialog: first !== undefined ? first.request : null,
|
|
398
392
|
agentDir: getAgentDir(),
|
|
399
|
-
recentUserMessages: messageLedger.recentUserMessages(),
|
|
400
393
|
}, true);
|
|
401
394
|
// No display_* replay: the retained extension state rides IN the snapshot
|
|
402
395
|
// (`snapshot.display`), so catch-up is one atomic frame. A viewer attaching
|
|
@@ -509,7 +502,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
509
502
|
rebind.setRebindSession();
|
|
510
503
|
await rebind.enqueueRebind(session, services);
|
|
511
504
|
const frameDispatch = createFrameDispatchContext({
|
|
512
|
-
nodeId, cfg, registry, toolGroups, projection,
|
|
505
|
+
nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion,
|
|
513
506
|
pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit,
|
|
514
507
|
persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec,
|
|
515
508
|
formatExactModelSpec, resolveLaunchModel,
|
|
@@ -6,11 +6,8 @@ export type LiveDeliverRoute = 'prompt' | 'steer';
|
|
|
6
6
|
export type LiveInterruptOutcome = 'turn' | 'bash' | 'idle';
|
|
7
7
|
/** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
|
|
8
8
|
* broker routes it itself (idle → prompt, streaming → steer) and acks once
|
|
9
|
-
* routed; resolves with the route taken.
|
|
10
|
-
|
|
11
|
-
* frame so the broker's ledger echoes it back on the relayed user
|
|
12
|
-
* `message_start` — distinct from the one-shot ack-correlation id. */
|
|
13
|
-
export declare function deliverLive(nodeId: string, text: string, messageId?: string): Promise<LiveDeliverRoute>;
|
|
9
|
+
* routed; resolves with the route taken. */
|
|
10
|
+
export declare function deliverLive(nodeId: string, text: string): Promise<LiveDeliverRoute>;
|
|
14
11
|
/** Abort whatever a LIVE node's engine is doing (turn or `!` bash). Resolves
|
|
15
12
|
* once the broker has PROCESSED the abort — which, on the single frame loop,
|
|
16
13
|
* also implies every earlier-accepted deliver was already routed (so a
|
|
@@ -14,17 +14,14 @@
|
|
|
14
14
|
import { oneShotControllerRequest } from './broker-request.js';
|
|
15
15
|
/** Deliver `text` to a LIVE node's engine on its serialized frame loop. The
|
|
16
16
|
* broker routes it itself (idle → prompt, streaming → steer) and acks once
|
|
17
|
-
* routed; resolves with the route taken.
|
|
18
|
-
|
|
19
|
-
* frame so the broker's ledger echoes it back on the relayed user
|
|
20
|
-
* `message_start` — distinct from the one-shot ack-correlation id. */
|
|
21
|
-
export function deliverLive(nodeId, text, messageId) {
|
|
17
|
+
* routed; resolves with the route taken. */
|
|
18
|
+
export function deliverLive(nodeId, text) {
|
|
22
19
|
return oneShotControllerRequest({
|
|
23
20
|
nodeId,
|
|
24
21
|
what: 'the interactive delivery',
|
|
25
22
|
ackFor: 'deliver',
|
|
26
23
|
correlated: true,
|
|
27
|
-
frame: (id) => ({ type: 'deliver', id, text
|
|
24
|
+
frame: (id) => ({ type: 'deliver', id, text }),
|
|
28
25
|
result: (detail) => (detail === 'steer' ? 'steer' : 'prompt'),
|
|
29
26
|
});
|
|
30
27
|
}
|
|
@@ -24,13 +24,10 @@ export interface SessionState {
|
|
|
24
24
|
/** Live context occupancy in prompt tokens: seeded from the snapshot's
|
|
25
25
|
* contextUsage, then tracked off each assistant `message_end`. */
|
|
26
26
|
contextTokens: number | undefined;
|
|
27
|
-
/** Queued steering + follow-up messages, from `queue_update`.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* A fresh broker with an empty queue emits no `queue_update`, so every
|
|
31
|
-
* welcome resets this rather than leaving a dead broker's rows on screen. */
|
|
27
|
+
/** Queued steering + follow-up messages, from `queue_update`. A fresh broker
|
|
28
|
+
* with an empty queue emits no `queue_update`, so every welcome resets this
|
|
29
|
+
* rather than leaving a dead broker's rows on screen. */
|
|
32
30
|
queued: Array<{
|
|
33
|
-
id?: string;
|
|
34
31
|
text: string;
|
|
35
32
|
}>;
|
|
36
33
|
display: DisplayState;
|
|
@@ -132,11 +132,7 @@ export function reduce(state, frame) {
|
|
|
132
132
|
case 'thinking_level_changed':
|
|
133
133
|
return patchEngine(state, { thinkingLevel: frame.level });
|
|
134
134
|
case 'queue_update': {
|
|
135
|
-
|
|
136
|
-
// pi's text arrays; zip them, tolerating their absence (older broker).
|
|
137
|
-
const { steeringIds, followUpIds } = frame;
|
|
138
|
-
const zip = (texts, ids) => texts.map((text, i) => (ids?.[i] !== undefined ? { id: ids[i], text } : { text }));
|
|
139
|
-
const queued = [...zip(frame.steering, steeringIds), ...zip(frame.followUp, followUpIds)];
|
|
135
|
+
const queued = [...frame.steering, ...frame.followUp].map((text) => ({ text }));
|
|
140
136
|
const next = patchEngine(state, { pendingMessageCount: queued.length });
|
|
141
137
|
return { ...next, queued };
|
|
142
138
|
}
|
|
@@ -59,12 +59,6 @@ function parseSendBody(body) {
|
|
|
59
59
|
if (delivery !== undefined && delivery !== 'interactive') {
|
|
60
60
|
throw usage(`invalid delivery: ${String(delivery)} (expected interactive)`);
|
|
61
61
|
}
|
|
62
|
-
const messageId = b['message_id'];
|
|
63
|
-
if (messageId !== undefined && typeof messageId !== 'string') {
|
|
64
|
-
throw usage('message_id must be a string');
|
|
65
|
-
}
|
|
66
|
-
if (typeof messageId === 'string' && messageId.trim() !== '')
|
|
67
|
-
out.message_id = messageId;
|
|
68
62
|
if (delivery === 'interactive') {
|
|
69
63
|
// Interactive delivery is a live-conversation send: a plain immediate body,
|
|
70
64
|
// nothing that only makes sense on the durable path.
|
|
@@ -164,7 +158,7 @@ async function handleMessage(ctx) {
|
|
|
164
158
|
// straight there (append + revive, watcher delivers post-boot). ---
|
|
165
159
|
if (req.delivery === 'interactive' && isBrokerLive(meta)) {
|
|
166
160
|
try {
|
|
167
|
-
await deliverLive(id, req.body
|
|
161
|
+
await deliverLive(id, req.body);
|
|
168
162
|
const body = {
|
|
169
163
|
node_id: id,
|
|
170
164
|
delivered: true,
|
package/dist/types.d.ts
CHANGED
|
@@ -33,7 +33,18 @@ export interface PluginManifest {
|
|
|
33
33
|
description?: string;
|
|
34
34
|
source?: string;
|
|
35
35
|
owner?: OwnerRef;
|
|
36
|
-
|
|
36
|
+
/** Kind-registry contributions, keyed by full kind string (top-level e.g.
|
|
37
|
+
* `applet-builder`, or sub-kind e.g. `plan/reviewers/security`). Each entry
|
|
38
|
+
* is either a FULL `KindConfig` (has `whenToUse`: defines or wholly shadows
|
|
39
|
+
* the kind) or a sparse PATCH over a kind a lower layer already defines —
|
|
40
|
+
* the same two shapes a scope `config.json` `kinds` block accepts.
|
|
41
|
+
* `readMergedLaunchConfig` layers an enabled plugin's entries directly
|
|
42
|
+
* above the builtin registry and below its host scope's own `config.json`,
|
|
43
|
+
* so a plugin can ship a persona kind (registry entry here + gated persona
|
|
44
|
+
* memory docs in its `memory/` tree) and the user still overrides it.
|
|
45
|
+
* Archive plugins declare this in `bundle.json`; the installer copies the
|
|
46
|
+
* validated block into this synthesized manifest. */
|
|
47
|
+
kinds?: Record<string, Partial<KindConfig>>;
|
|
37
48
|
/** Plugin-root-relative path to one declarative command manifest
|
|
38
49
|
* (`commands.json`). When present on an installed, enabled plugin, that
|
|
39
50
|
* manifest contributes top-level command branches to the `crtr` tree —
|
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.187",
|
|
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.187",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/** One id-bearing message the ledger tracks. */
|
|
2
|
-
export interface LedgerEntry {
|
|
3
|
-
id: string;
|
|
4
|
-
text: string;
|
|
5
|
-
}
|
|
6
|
-
export declare class MessageIdLedger {
|
|
7
|
-
private steering;
|
|
8
|
-
private followUp;
|
|
9
|
-
private pendingIntents;
|
|
10
|
-
private stagedRetirementId;
|
|
11
|
-
private recent;
|
|
12
|
-
/** Record an accepted client frame's identity before its engine call runs.
|
|
13
|
-
* `id` is the sender's `message_id` when it minted one, else broker-minted. */
|
|
14
|
-
noteDispatch(id: string, text: string): void;
|
|
15
|
-
/** Remove a noted intent whose engine call rejected — the message will never
|
|
16
|
-
* appear in a queue or as a user message. No-op if already adopted. */
|
|
17
|
-
dropIntent(id: string): void;
|
|
18
|
-
/**
|
|
19
|
-
* Fold one pi `queue_update` into the ledger and return the id arrays
|
|
20
|
-
* parallel to its text arrays (total: every position gets an id).
|
|
21
|
-
*
|
|
22
|
-
* Per-queue positional diff against the mirrored entries: a text matching an
|
|
23
|
-
* old entry at-or-after the cursor keeps its id (everything skipped is
|
|
24
|
-
* removed); an unmatched text ADOPTS the first pending intent with equal
|
|
25
|
-
* text, else mints a fresh id. Exactly ONE removal across both queues is
|
|
26
|
-
* pi's per-message retirement (its queue_update precedes the user
|
|
27
|
-
* `message_start` it belongs to) and is staged for `takeUserMessageId`;
|
|
28
|
-
* 0 or >1 removals (an enqueue, clearQueue/dequeue, abort) stage nothing.
|
|
29
|
-
*/
|
|
30
|
-
reconcileQueueUpdate(steering: readonly string[], followUp: readonly string[]): {
|
|
31
|
-
steeringIds: string[];
|
|
32
|
-
followUpIds: string[];
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* The id for a relayed user `message_start`, or undefined when this message
|
|
36
|
-
* carries none (an engine-command expansion, a text pi transformed, or a
|
|
37
|
-
* pre-ledger replay). A staged retirement wins — pi emitted its queue_update
|
|
38
|
-
* synchronously right before this message; otherwise the first pending
|
|
39
|
-
* intent with equal text (the idle-prompt case, which never touches a
|
|
40
|
-
* queue). Id-bearing results are remembered for `recentUserMessages`.
|
|
41
|
-
*/
|
|
42
|
-
takeUserMessageId(messageText: string): string | undefined;
|
|
43
|
-
/** The last {@link MAX_RECENT} id-bearing user-message dispatches, oldest
|
|
44
|
-
* first — the `welcome.recentUserMessages` payload. */
|
|
45
|
-
recentUserMessages(): Array<{
|
|
46
|
-
id: string;
|
|
47
|
-
text: string;
|
|
48
|
-
}>;
|
|
49
|
-
/** Session rebind/replacement: the mirrored queues, staged retirement, and
|
|
50
|
-
* recent list all described the OLD session — drop everything. */
|
|
51
|
-
reset(): void;
|
|
52
|
-
private remember;
|
|
53
|
-
}
|