@crouton-kit/crouter 0.3.17 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import type { AgentSession, AgentSessionEvent, SessionStats } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { ImageContent } from '@earendil-works/pi-ai';
|
|
3
|
+
/** Emitted when an extension needs user input (mirror of pi rpc-types.d.ts). */
|
|
4
|
+
export type RpcExtensionUIRequest = {
|
|
5
|
+
type: 'extension_ui_request';
|
|
6
|
+
id: string;
|
|
7
|
+
method: 'select';
|
|
8
|
+
title: string;
|
|
9
|
+
options: string[];
|
|
10
|
+
timeout?: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'extension_ui_request';
|
|
13
|
+
id: string;
|
|
14
|
+
method: 'confirm';
|
|
15
|
+
title: string;
|
|
16
|
+
message: string;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'extension_ui_request';
|
|
20
|
+
id: string;
|
|
21
|
+
method: 'input';
|
|
22
|
+
title: string;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
timeout?: number;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'extension_ui_request';
|
|
27
|
+
id: string;
|
|
28
|
+
method: 'editor';
|
|
29
|
+
title: string;
|
|
30
|
+
prefill?: string;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'extension_ui_request';
|
|
33
|
+
id: string;
|
|
34
|
+
method: 'notify';
|
|
35
|
+
message: string;
|
|
36
|
+
notifyType?: 'info' | 'warning' | 'error';
|
|
37
|
+
} | {
|
|
38
|
+
type: 'extension_ui_request';
|
|
39
|
+
id: string;
|
|
40
|
+
method: 'setStatus';
|
|
41
|
+
statusKey: string;
|
|
42
|
+
statusText: string | undefined;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'extension_ui_request';
|
|
45
|
+
id: string;
|
|
46
|
+
method: 'setWidget';
|
|
47
|
+
widgetKey: string;
|
|
48
|
+
widgetLines: string[] | undefined;
|
|
49
|
+
widgetPlacement?: 'aboveEditor' | 'belowEditor';
|
|
50
|
+
} | {
|
|
51
|
+
type: 'extension_ui_request';
|
|
52
|
+
id: string;
|
|
53
|
+
method: 'setTitle';
|
|
54
|
+
title: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'extension_ui_request';
|
|
57
|
+
id: string;
|
|
58
|
+
method: 'set_editor_text';
|
|
59
|
+
text: string;
|
|
60
|
+
};
|
|
61
|
+
/** Response to an extension UI request (mirror of pi rpc-types.d.ts). */
|
|
62
|
+
export type RpcExtensionUIResponse = {
|
|
63
|
+
type: 'extension_ui_response';
|
|
64
|
+
id: string;
|
|
65
|
+
value: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'extension_ui_response';
|
|
68
|
+
id: string;
|
|
69
|
+
confirmed: boolean;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'extension_ui_response';
|
|
72
|
+
id: string;
|
|
73
|
+
cancelled: true;
|
|
74
|
+
};
|
|
75
|
+
/** Single controller (drives the engine) + N read-only observers (§5.3). */
|
|
76
|
+
export type ClientRole = 'controller' | 'observer';
|
|
77
|
+
/** The full state a (re)attaching client needs to catch up instantly — the
|
|
78
|
+
* broker's authoritative in-memory view (design §5.2: get_messages +
|
|
79
|
+
* get_state + get_session_stats). `messages` is pi's `AgentMessage[]` (the
|
|
80
|
+
* `session.messages` getter type), kept structural to avoid a deep type
|
|
81
|
+
* import. */
|
|
82
|
+
export interface BrokerSnapshot {
|
|
83
|
+
messages: AgentSession['messages'];
|
|
84
|
+
stats: SessionStats;
|
|
85
|
+
state: {
|
|
86
|
+
sessionId: string;
|
|
87
|
+
sessionFile: string | undefined;
|
|
88
|
+
model: string | undefined;
|
|
89
|
+
isStreaming: boolean;
|
|
90
|
+
thinkingLevel: AgentSession['thinkingLevel'];
|
|
91
|
+
steeringMode: AgentSession['steeringMode'];
|
|
92
|
+
followUpMode: AgentSession['followUpMode'];
|
|
93
|
+
sessionName: AgentSession['sessionName'];
|
|
94
|
+
autoCompactionEnabled: AgentSession['autoCompactionEnabled'];
|
|
95
|
+
pendingMessageCount: AgentSession['pendingMessageCount'];
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export interface HelloFrame {
|
|
99
|
+
type: 'hello';
|
|
100
|
+
role: ClientRole;
|
|
101
|
+
client_id: string;
|
|
102
|
+
/** Terminal geometry of a tmux-pane viewer; absent for a browser/headless. */
|
|
103
|
+
term?: {
|
|
104
|
+
cols: number;
|
|
105
|
+
rows: number;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Drive the engine — controller only. Map 1:1 to session.prompt/steer/followUp/abort.
|
|
109
|
+
* `images` carries pasted/attached images to the engine (review M1): pi accepts
|
|
110
|
+
* `prompt(text,{images})` / `steer(text,images)` / `followUp(text,images)` at
|
|
111
|
+
* 0.78.1. The wire TYPE lives here; T3/T6 wire the runtime side. The BROKER read
|
|
112
|
+
* cap (`BROKER_READ_CAPS`) is sized to hold a resizeImage-bounded PNG's base64. */
|
|
113
|
+
export interface PromptFrame {
|
|
114
|
+
type: 'prompt';
|
|
115
|
+
text: string;
|
|
116
|
+
images?: ImageContent[];
|
|
117
|
+
}
|
|
118
|
+
export interface SteerFrame {
|
|
119
|
+
type: 'steer';
|
|
120
|
+
text: string;
|
|
121
|
+
images?: ImageContent[];
|
|
122
|
+
}
|
|
123
|
+
export interface FollowUpFrame {
|
|
124
|
+
type: 'follow_up';
|
|
125
|
+
text: string;
|
|
126
|
+
images?: ImageContent[];
|
|
127
|
+
}
|
|
128
|
+
export interface AbortFrame {
|
|
129
|
+
type: 'abort';
|
|
130
|
+
}
|
|
131
|
+
/** Controller arbitration (§5.3). */
|
|
132
|
+
export interface RequestControlFrame {
|
|
133
|
+
type: 'request_control';
|
|
134
|
+
}
|
|
135
|
+
export interface ReleaseControlFrame {
|
|
136
|
+
type: 'release_control';
|
|
137
|
+
}
|
|
138
|
+
/** Detach this client — the engine runs on (distinct from `shutdown`). */
|
|
139
|
+
export interface ByeFrame {
|
|
140
|
+
type: 'bye';
|
|
141
|
+
}
|
|
142
|
+
/** Graceful teardown: dispose the engine + exit the broker (design §3.3, the
|
|
143
|
+
* `HeadlessBrokerHost.teardown` happy path). A FIRST-CLASS control frame,
|
|
144
|
+
* distinct from `bye` (which only drops one listener). §5.2's wire table omits
|
|
145
|
+
* it; the plan reconciles §3.3's "graceful teardown over the socket" by
|
|
146
|
+
* treating `shutdown` as a client→broker control frame. */
|
|
147
|
+
export interface ShutdownFrame {
|
|
148
|
+
type: 'shutdown';
|
|
149
|
+
}
|
|
150
|
+
/** `setModel(model)` — `/model` (selector resolves → chosen id). */
|
|
151
|
+
export interface SetModelFrame {
|
|
152
|
+
type: 'set_model';
|
|
153
|
+
model: string;
|
|
154
|
+
}
|
|
155
|
+
/** `cycleModel()`. */
|
|
156
|
+
export interface CycleModelFrame {
|
|
157
|
+
type: 'cycle_model';
|
|
158
|
+
}
|
|
159
|
+
/** `setThinkingLevel(level)` — `/settings` thinking. */
|
|
160
|
+
export interface SetThinkingLevelFrame {
|
|
161
|
+
type: 'set_thinking_level';
|
|
162
|
+
level: AgentSession['thinkingLevel'];
|
|
163
|
+
}
|
|
164
|
+
/** `setAutoRetryEnabled(enabled)`. */
|
|
165
|
+
export interface SetAutoRetryFrame {
|
|
166
|
+
type: 'set_auto_retry';
|
|
167
|
+
enabled: boolean;
|
|
168
|
+
}
|
|
169
|
+
/** `setAutoCompactionEnabled(enabled)` — `/settings`. */
|
|
170
|
+
export interface SetAutoCompactionFrame {
|
|
171
|
+
type: 'set_auto_compaction';
|
|
172
|
+
enabled: boolean;
|
|
173
|
+
}
|
|
174
|
+
/** `compact(instructions?)` — `/compact`. */
|
|
175
|
+
export interface CompactFrame {
|
|
176
|
+
type: 'compact';
|
|
177
|
+
instructions?: string;
|
|
178
|
+
}
|
|
179
|
+
/** `runtimeHost.newSession()` + rebind — `/new`. */
|
|
180
|
+
export interface NewSessionFrame {
|
|
181
|
+
type: 'new_session';
|
|
182
|
+
}
|
|
183
|
+
/** `runtimeHost.switchSession(path)` + rebind — `/resume` (selector). */
|
|
184
|
+
export interface SwitchSessionFrame {
|
|
185
|
+
type: 'switch_session';
|
|
186
|
+
path: string;
|
|
187
|
+
}
|
|
188
|
+
/** `runtimeHost.fork(entryId)` + rebind — `/fork` (selector). */
|
|
189
|
+
export interface ForkFrame {
|
|
190
|
+
type: 'fork';
|
|
191
|
+
entryId: string;
|
|
192
|
+
}
|
|
193
|
+
/** `setSessionName(name)` — `/name`. */
|
|
194
|
+
export interface SetSessionNameFrame {
|
|
195
|
+
type: 'set_session_name';
|
|
196
|
+
name: string;
|
|
197
|
+
}
|
|
198
|
+
/** Registered commands + templates + skills, MERGED with BUILTIN_SLASH_COMMANDS
|
|
199
|
+
* (M9: RPC omits builtins) — drives autocomplete. Broker replies via `ack`. */
|
|
200
|
+
export interface GetCommandsFrame {
|
|
201
|
+
type: 'get_commands';
|
|
202
|
+
}
|
|
203
|
+
/** `navigateTree(targetId, options?)` — `/tree`. Options mirror AgentSession.navigateTree. */
|
|
204
|
+
export interface NavigateTreeFrame {
|
|
205
|
+
type: 'navigate_tree';
|
|
206
|
+
targetId: string;
|
|
207
|
+
options?: {
|
|
208
|
+
summarize?: boolean;
|
|
209
|
+
customInstructions?: string;
|
|
210
|
+
replaceInstructions?: boolean;
|
|
211
|
+
label?: string;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** `reload()` — `/reload`. */
|
|
215
|
+
export interface ReloadFrame {
|
|
216
|
+
type: 'reload';
|
|
217
|
+
}
|
|
218
|
+
/** `exportToHtml(path)` / `exportToJsonl(path)` — `/export`. */
|
|
219
|
+
export interface ExportFrame {
|
|
220
|
+
type: 'export';
|
|
221
|
+
path: string;
|
|
222
|
+
format: 'html' | 'jsonl';
|
|
223
|
+
}
|
|
224
|
+
/** Answer a blocking extension dialog — pi's public RPC response type. */
|
|
225
|
+
export type ExtensionUIResponseFrame = RpcExtensionUIResponse;
|
|
226
|
+
export type ClientToBroker = HelloFrame | PromptFrame | SteerFrame | FollowUpFrame | AbortFrame | RequestControlFrame | ReleaseControlFrame | ByeFrame | ShutdownFrame | SetModelFrame | CycleModelFrame | SetThinkingLevelFrame | SetAutoRetryFrame | SetAutoCompactionFrame | CompactFrame | NewSessionFrame | SwitchSessionFrame | ForkFrame | SetSessionNameFrame | GetCommandsFrame | NavigateTreeFrame | ReloadFrame | ExportFrame | ExtensionUIResponseFrame;
|
|
227
|
+
export interface WelcomeFrame {
|
|
228
|
+
type: 'welcome';
|
|
229
|
+
snapshot: BrokerSnapshot;
|
|
230
|
+
role: ClientRole;
|
|
231
|
+
controller_id: string | null;
|
|
232
|
+
/** A dialog already in-flight when this client attached (Phase 4
|
|
233
|
+
* attach-mid-dialog). The field exists now; it is only ever populated in
|
|
234
|
+
* Phase 4 — Phase 3 always sends it absent/null. */
|
|
235
|
+
pending_dialog?: RpcExtensionUIRequest | null;
|
|
236
|
+
}
|
|
237
|
+
export interface ControlChangedFrame {
|
|
238
|
+
type: 'control_changed';
|
|
239
|
+
controller_id: string | null;
|
|
240
|
+
}
|
|
241
|
+
export interface ErrorFrame {
|
|
242
|
+
type: 'error';
|
|
243
|
+
code: string;
|
|
244
|
+
message: string;
|
|
245
|
+
}
|
|
246
|
+
/** Result of a controller command op (§1.3): `for` echoes the op name, `ok` the
|
|
247
|
+
* outcome, `detail` an optional human-readable note. */
|
|
248
|
+
export interface AckFrame {
|
|
249
|
+
type: 'ack';
|
|
250
|
+
for: string;
|
|
251
|
+
ok: boolean;
|
|
252
|
+
detail?: string;
|
|
253
|
+
}
|
|
254
|
+
/** A blocking dialog routed to the controller — pi's public RPC request type. */
|
|
255
|
+
export type ExtensionUIRequestFrame = RpcExtensionUIRequest;
|
|
256
|
+
/** Everything the broker can send. Live `AgentSessionEvent`s are relayed
|
|
257
|
+
* verbatim (the broker adds nothing); the broker's own control frames carry
|
|
258
|
+
* non-colliding `type` discriminants. */
|
|
259
|
+
export type BrokerToClient = WelcomeFrame | ControlChangedFrame | ErrorFrame | AckFrame | ExtensionUIRequestFrame | AgentSessionEvent;
|
|
260
|
+
/** Encode one frame as a single newline-terminated JSON line. */
|
|
261
|
+
export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
|
|
262
|
+
/** Byte bounds for a {@link FrameDecoder} (C5). */
|
|
263
|
+
export interface FrameDecoderCaps {
|
|
264
|
+
/** Largest single (unterminated) frame the decoder will buffer before throwing. */
|
|
265
|
+
maxLineBytes: number;
|
|
266
|
+
/** Largest the internal buffer may peak to in one push before throwing. */
|
|
267
|
+
maxTotalBytes: number;
|
|
268
|
+
}
|
|
269
|
+
/** Thrown by {@link FrameDecoder.push} when a peer's buffered bytes exceed a cap
|
|
270
|
+
* (C5). The caller catches it, best-effort sends `error{code:'frame_overflow'}`,
|
|
271
|
+
* and DESTROYS that socket — cap-and-drop the peer, never grow-to-OOM. `kind`
|
|
272
|
+
* says which bound tripped: a single oversized frame (`line`) or total backlog
|
|
273
|
+
* (`total`). */
|
|
274
|
+
export declare class FrameOverflowError extends Error {
|
|
275
|
+
readonly kind: 'line' | 'total';
|
|
276
|
+
readonly bytes: number;
|
|
277
|
+
readonly cap: number;
|
|
278
|
+
constructor(kind: 'line' | 'total', bytes: number, cap: number);
|
|
279
|
+
}
|
|
280
|
+
/** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
|
|
281
|
+
* full message history and can be many MiB, so these are generous. Covers
|
|
282
|
+
* realistic long sessions; snapshot CHUNKING is deferred (plan §1.1 known
|
|
283
|
+
* limitation, review m3 — the long-lived broker makes a big welcome a realistic,
|
|
284
|
+
* not pathological, trigger, but the fixed cap is accepted for Phase 4). */
|
|
285
|
+
export declare const CLIENT_READ_CAPS: FrameDecoderCaps;
|
|
286
|
+
/** Caps the BROKER uses reading CLIENT frames. Plan §1.1 specified a tight
|
|
287
|
+
* 4/16 MiB, but review M1 requires image-paste frames to fit: a resizeImage-
|
|
288
|
+
* bounded PNG's base64 is a few MiB and would clip a 4 MiB line cap. So these are
|
|
289
|
+
* RAISED above the plan's 4/16 to hold an image-bearing `prompt`/`steer`/
|
|
290
|
+
* `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame
|
|
291
|
+
* is cap-and-dropped, never grow-to-OOM (C5). */
|
|
292
|
+
export declare const BROKER_READ_CAPS: FrameDecoderCaps;
|
|
293
|
+
/** Incremental newline-delimited JSON reader: feed raw socket chunks, get back
|
|
294
|
+
* the complete frames decoded so far. Buffers a partial trailing line across
|
|
295
|
+
* pushes; silently drops a malformed (unparseable) line (a viewer must never
|
|
296
|
+
* crash the broker on bad JSON). The caller narrows the `unknown` against the
|
|
297
|
+
* frame unions.
|
|
298
|
+
*
|
|
299
|
+
* BOUNDED (C5): an oversized buffer is the THROW case. If a single push peaks the
|
|
300
|
+
* internal buffer above `maxTotalBytes`, or the surviving (unterminated) partial
|
|
301
|
+
* line exceeds `maxLineBytes`, {@link push} throws {@link FrameOverflowError}
|
|
302
|
+
* instead of returning frames — the caller drops that peer rather than letting a
|
|
303
|
+
* malicious/buggy stream grow the buffer to OOM. Bytes are measured with
|
|
304
|
+
* `Buffer.byteLength`, never string length; a running counter makes each push's
|
|
305
|
+
* size CHECK O(1) (no re-scan of the whole buffer to measure it).
|
|
306
|
+
*
|
|
307
|
+
* AMORTIZED O(total bytes) (perf regression fix, 2026-06-09): the partial line
|
|
308
|
+
* carried across pushes is held as an ARRAY of chunk strings (`parts`), joined
|
|
309
|
+
* only when its terminating `\n` arrives. The `\n` scan looks at each incoming
|
|
310
|
+
* chunk ONCE — never re-scanning the accumulated carry — so a multi-MiB frame
|
|
311
|
+
* arriving in ~64 KiB socket chunks costs O(frame) total instead of O(frame ×
|
|
312
|
+
* chunks). The old `buf += chunk; buf.indexOf('\n')` shape flattened + re-walked
|
|
313
|
+
* the whole carry per chunk: a 16 MiB welcome snapshot cost ~250 ms of
|
|
314
|
+
* event-loop stall in `crtr attach` (typing lag) and the same again in the
|
|
315
|
+
* broker; this shape decodes it in ~13 ms.
|
|
316
|
+
*
|
|
317
|
+
* A `StringDecoder` decodes incoming Buffer chunks: an incomplete trailing
|
|
318
|
+
* multibyte sequence is held back across pushes instead of being mangled to
|
|
319
|
+
* U+FFFD, so a char split at a `node:net` chunk boundary is never corrupted, and
|
|
320
|
+
* `partBytes` counts decoded-string bytes (a raw `chunk.length` would desync
|
|
321
|
+
* and weaken the C5 cap on multibyte/invalid input). */
|
|
322
|
+
export declare class FrameDecoder {
|
|
323
|
+
private readonly caps;
|
|
324
|
+
/** The unterminated partial line carried across pushes, as unjoined chunks
|
|
325
|
+
* (never contains a '\n'). Joined lazily when its newline arrives. */
|
|
326
|
+
private parts;
|
|
327
|
+
/** Byte length (Buffer.byteLength) of `parts` joined — kept in exact lockstep. */
|
|
328
|
+
private partBytes;
|
|
329
|
+
private readonly utf8;
|
|
330
|
+
constructor(caps: FrameDecoderCaps);
|
|
331
|
+
push(chunk: Buffer | string): unknown[];
|
|
332
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// broker-protocol.ts — the client↔broker wire protocol for the headless node
|
|
2
|
+
// Surface (design §5.2). Pure types + a newline-delimited JSON codec; no I/O, no
|
|
3
|
+
// SDK construction. Consumed by the broker (src/core/runtime/broker.ts) and, in
|
|
4
|
+
// Phase 4, by the `crtr attach` terminal client and the `crtr web` bridge.
|
|
5
|
+
//
|
|
6
|
+
// The transport is one unix socket per node (`nodeDir(id)/view.sock`) speaking
|
|
7
|
+
// newline-delimited JSON frames. Live engine events are relayed VERBATIM — the
|
|
8
|
+
// broker is a transparent multiplexer, so a broker→client frame is either one of
|
|
9
|
+
// the broker's own control frames (welcome/control_changed/error) or a raw pi
|
|
10
|
+
// `AgentSessionEvent` / `extension_ui_request`. The `type` namespaces never
|
|
11
|
+
// collide, so the union below stays a clean discriminated union.
|
|
12
|
+
//
|
|
13
|
+
// The extension-dialog frames mirror pi's RPC types (decision §1.15). pi names
|
|
14
|
+
// the correlation field `id` (design §5.2 wrote `request_id`); we follow pi.
|
|
15
|
+
//
|
|
16
|
+
// NOTE (version-skew, 2026-06-08): §1.15 says to IMPORT pi's now-public
|
|
17
|
+
// `RpcExtensionUIRequest`/`RpcExtensionUIResponse`. The newer pi build re-exports
|
|
18
|
+
// them from the package root, but the npm-published `0.78.1` currently in
|
|
19
|
+
// crouter's node_modules defines them in `dist/modes/rpc/rpc-types.d.ts` WITHOUT
|
|
20
|
+
// re-exporting them (the `exports` map gates off deep subpaths, so even a
|
|
21
|
+
// type-only deep import is unresolvable). They are therefore mirrored locally
|
|
22
|
+
// below — byte-for-byte from pi's `rpc-types.d.ts`. When the published build
|
|
23
|
+
// catches up, delete these two declarations and import them from the package
|
|
24
|
+
// root; nothing else changes (same names, same shapes).
|
|
25
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Newline-delimited JSON codec (the framing pi solves with `jsonl`, reproduced
|
|
28
|
+
// locally so the protocol module owns no transport dependency)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/** Encode one frame as a single newline-terminated JSON line. */
|
|
31
|
+
export function encodeFrame(frame) {
|
|
32
|
+
return JSON.stringify(frame) + '\n';
|
|
33
|
+
}
|
|
34
|
+
/** Thrown by {@link FrameDecoder.push} when a peer's buffered bytes exceed a cap
|
|
35
|
+
* (C5). The caller catches it, best-effort sends `error{code:'frame_overflow'}`,
|
|
36
|
+
* and DESTROYS that socket — cap-and-drop the peer, never grow-to-OOM. `kind`
|
|
37
|
+
* says which bound tripped: a single oversized frame (`line`) or total backlog
|
|
38
|
+
* (`total`). */
|
|
39
|
+
export class FrameOverflowError extends Error {
|
|
40
|
+
kind;
|
|
41
|
+
bytes;
|
|
42
|
+
cap;
|
|
43
|
+
constructor(kind, bytes, cap) {
|
|
44
|
+
super(`frame decoder overflow (${kind}): ${bytes} bytes exceeds cap ${cap}`);
|
|
45
|
+
this.kind = kind;
|
|
46
|
+
this.bytes = bytes;
|
|
47
|
+
this.cap = cap;
|
|
48
|
+
this.name = 'FrameOverflowError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
|
|
52
|
+
* full message history and can be many MiB, so these are generous. Covers
|
|
53
|
+
* realistic long sessions; snapshot CHUNKING is deferred (plan §1.1 known
|
|
54
|
+
* limitation, review m3 — the long-lived broker makes a big welcome a realistic,
|
|
55
|
+
* not pathological, trigger, but the fixed cap is accepted for Phase 4). */
|
|
56
|
+
export const CLIENT_READ_CAPS = {
|
|
57
|
+
maxLineBytes: 256 * 1024 * 1024,
|
|
58
|
+
maxTotalBytes: 256 * 1024 * 1024,
|
|
59
|
+
};
|
|
60
|
+
/** Caps the BROKER uses reading CLIENT frames. Plan §1.1 specified a tight
|
|
61
|
+
* 4/16 MiB, but review M1 requires image-paste frames to fit: a resizeImage-
|
|
62
|
+
* bounded PNG's base64 is a few MiB and would clip a 4 MiB line cap. So these are
|
|
63
|
+
* RAISED above the plan's 4/16 to hold an image-bearing `prompt`/`steer`/
|
|
64
|
+
* `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame
|
|
65
|
+
* is cap-and-dropped, never grow-to-OOM (C5). */
|
|
66
|
+
export const BROKER_READ_CAPS = {
|
|
67
|
+
maxLineBytes: 24 * 1024 * 1024,
|
|
68
|
+
maxTotalBytes: 48 * 1024 * 1024,
|
|
69
|
+
};
|
|
70
|
+
/** Incremental newline-delimited JSON reader: feed raw socket chunks, get back
|
|
71
|
+
* the complete frames decoded so far. Buffers a partial trailing line across
|
|
72
|
+
* pushes; silently drops a malformed (unparseable) line (a viewer must never
|
|
73
|
+
* crash the broker on bad JSON). The caller narrows the `unknown` against the
|
|
74
|
+
* frame unions.
|
|
75
|
+
*
|
|
76
|
+
* BOUNDED (C5): an oversized buffer is the THROW case. If a single push peaks the
|
|
77
|
+
* internal buffer above `maxTotalBytes`, or the surviving (unterminated) partial
|
|
78
|
+
* line exceeds `maxLineBytes`, {@link push} throws {@link FrameOverflowError}
|
|
79
|
+
* instead of returning frames — the caller drops that peer rather than letting a
|
|
80
|
+
* malicious/buggy stream grow the buffer to OOM. Bytes are measured with
|
|
81
|
+
* `Buffer.byteLength`, never string length; a running counter makes each push's
|
|
82
|
+
* size CHECK O(1) (no re-scan of the whole buffer to measure it).
|
|
83
|
+
*
|
|
84
|
+
* AMORTIZED O(total bytes) (perf regression fix, 2026-06-09): the partial line
|
|
85
|
+
* carried across pushes is held as an ARRAY of chunk strings (`parts`), joined
|
|
86
|
+
* only when its terminating `\n` arrives. The `\n` scan looks at each incoming
|
|
87
|
+
* chunk ONCE — never re-scanning the accumulated carry — so a multi-MiB frame
|
|
88
|
+
* arriving in ~64 KiB socket chunks costs O(frame) total instead of O(frame ×
|
|
89
|
+
* chunks). The old `buf += chunk; buf.indexOf('\n')` shape flattened + re-walked
|
|
90
|
+
* the whole carry per chunk: a 16 MiB welcome snapshot cost ~250 ms of
|
|
91
|
+
* event-loop stall in `crtr attach` (typing lag) and the same again in the
|
|
92
|
+
* broker; this shape decodes it in ~13 ms.
|
|
93
|
+
*
|
|
94
|
+
* A `StringDecoder` decodes incoming Buffer chunks: an incomplete trailing
|
|
95
|
+
* multibyte sequence is held back across pushes instead of being mangled to
|
|
96
|
+
* U+FFFD, so a char split at a `node:net` chunk boundary is never corrupted, and
|
|
97
|
+
* `partBytes` counts decoded-string bytes (a raw `chunk.length` would desync
|
|
98
|
+
* and weaken the C5 cap on multibyte/invalid input). */
|
|
99
|
+
export class FrameDecoder {
|
|
100
|
+
caps;
|
|
101
|
+
/** The unterminated partial line carried across pushes, as unjoined chunks
|
|
102
|
+
* (never contains a '\n'). Joined lazily when its newline arrives. */
|
|
103
|
+
parts = [];
|
|
104
|
+
/** Byte length (Buffer.byteLength) of `parts` joined — kept in exact lockstep. */
|
|
105
|
+
partBytes = 0;
|
|
106
|
+
utf8 = new StringDecoder('utf8');
|
|
107
|
+
constructor(caps) {
|
|
108
|
+
this.caps = caps;
|
|
109
|
+
}
|
|
110
|
+
push(chunk) {
|
|
111
|
+
// A string input is already complete text; a Buffer goes through the
|
|
112
|
+
// StringDecoder so a boundary-split multibyte char is buffered, not corrupted.
|
|
113
|
+
const str = typeof chunk === 'string' ? chunk : this.utf8.write(chunk);
|
|
114
|
+
const strBytes = Buffer.byteLength(str);
|
|
115
|
+
// Peak-buffer bound: caps the most we ever hold in one push (carry + this
|
|
116
|
+
// entire chunk, before draining complete frames) — identical accounting to
|
|
117
|
+
// the old whole-buffer check.
|
|
118
|
+
if (this.partBytes + strBytes > this.caps.maxTotalBytes) {
|
|
119
|
+
throw new FrameOverflowError('total', this.partBytes + strBytes, this.caps.maxTotalBytes);
|
|
120
|
+
}
|
|
121
|
+
const out = [];
|
|
122
|
+
let from = 0; // scan offset into `str` — each chunk is scanned exactly once
|
|
123
|
+
let nl;
|
|
124
|
+
while ((nl = str.indexOf('\n', from)) >= 0) {
|
|
125
|
+
const tail = str.slice(from, nl);
|
|
126
|
+
// First line of the push completes the carried partial; later lines live
|
|
127
|
+
// entirely inside `str` (parts is empty after the first join).
|
|
128
|
+
const line = (this.parts.length > 0 ? this.parts.join('') + tail : tail).trim();
|
|
129
|
+
this.parts.length = 0;
|
|
130
|
+
this.partBytes = 0;
|
|
131
|
+
from = nl + 1;
|
|
132
|
+
if (line === '')
|
|
133
|
+
continue;
|
|
134
|
+
try {
|
|
135
|
+
out.push(JSON.parse(line));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
/* drop a malformed frame — never throw out of the reader for bad JSON */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (from < str.length) {
|
|
142
|
+
const rest = from === 0 ? str : str.slice(from);
|
|
143
|
+
this.parts.push(rest);
|
|
144
|
+
this.partBytes += from === 0 ? strBytes : Buffer.byteLength(rest);
|
|
145
|
+
}
|
|
146
|
+
// Single-frame bound: a partial line that has grown past one frame's cap is a
|
|
147
|
+
// peer streaming bytes with no newline — drop it rather than buffer forever.
|
|
148
|
+
if (this.partBytes > this.caps.maxLineBytes) {
|
|
149
|
+
throw new FrameOverflowError('line', this.partBytes, this.caps.maxLineBytes);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
export { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, };
|
|
3
|
+
/**
|
|
4
|
+
* The minimal slice of the pi SDK the broker needs. Both the real package and the
|
|
5
|
+
* T11 `fake-engine` fixture satisfy this shape, so the broker can be driven by
|
|
6
|
+
* either via the `CRTR_BROKER_ENGINE` seam below.
|
|
7
|
+
*
|
|
8
|
+
* C3 (viewer-reuse scout `mq5thyli`): the broker drives the SERVICES path
|
|
9
|
+
* (`createAgentSessionServices` → `createAgentSessionFromServices`), NOT plain
|
|
10
|
+
* `createAgentSession`. Only the services path runs `registerProvider` (extension
|
|
11
|
+
* model-providers) + `applyExtensionFlagValues`, so a node whose model comes from
|
|
12
|
+
* a custom-provider extension actually gets a model. Mirrors pi `main.js`.
|
|
13
|
+
*/
|
|
14
|
+
export interface BrokerEngine {
|
|
15
|
+
createAgentSessionServices: typeof createAgentSessionServices;
|
|
16
|
+
createAgentSessionFromServices: typeof createAgentSessionFromServices;
|
|
17
|
+
/**
|
|
18
|
+
* The session-replacement runtime factory (T3 new_session/switch_session/fork).
|
|
19
|
+
* OPTIONAL: the real SDK (0.78.1) exposes it, so production gets full session
|
|
20
|
+
* replacement; the `fake-engine` test fixture does NOT provide it, so the
|
|
21
|
+
* broker degrades those three ops to an `error{engine_error}` reply rather than
|
|
22
|
+
* failing engine validation. Everything else works on both.
|
|
23
|
+
*/
|
|
24
|
+
createAgentSessionRuntime?: typeof createAgentSessionRuntime;
|
|
25
|
+
SessionManager: typeof SessionManager;
|
|
26
|
+
VERSION: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the broker's engine module. Defaults to the real SDK; honors the
|
|
30
|
+
* `CRTR_BROKER_ENGINE` env var (T11 test seam) so a lifecycle test can point the
|
|
31
|
+
* broker at `fixtures/fake-engine.ts` without touching production code. This is the
|
|
32
|
+
* sole dynamic-import indirection point — keep all engine resolution flowing
|
|
33
|
+
* through here so the seam stays a single, auditable swap.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadBrokerEngine(): Promise<BrokerEngine>;
|
|
36
|
+
/**
|
|
37
|
+
* Boot-time tripwire: compare the imported SDK `VERSION` against the `pi` binary
|
|
38
|
+
* crouter forks elsewhere (`pi --version`). On mismatch (or if the binary can't be
|
|
39
|
+
* probed) it logs a LOUD warning to stderr and proceeds — it NEVER throws. The v3
|
|
40
|
+
* session format auto-migrates on load, so minor skew round-trips; a hard fail here
|
|
41
|
+
* would risk a grace-revive crash loop. Phase 5 may harden this to fail-fast once
|
|
42
|
+
* the broker is the only host.
|
|
43
|
+
*
|
|
44
|
+
* @param engineVersion the version actually loaded (defaults to the statically
|
|
45
|
+
* imported `VERSION`; pass `engine.VERSION` from `loadBrokerEngine` to assert the
|
|
46
|
+
* version the broker is really driving).
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertEngineVersion(engineVersion?: string): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// broker-sdk.ts — the single import-and-version-assert shim between the headless
|
|
2
|
+
// broker and the pi SDK (`@earendil-works/pi-coding-agent`).
|
|
3
|
+
//
|
|
4
|
+
// crouter has never imported the SDK before — it only ever forked the `pi` binary
|
|
5
|
+
// (see `launch.ts buildPiArgv`). The broker (T4) is the first consumer. This shim
|
|
6
|
+
// is the one place the dependency is named, so:
|
|
7
|
+
// - the version tripwire (`assertEngineVersion`) lives here, and
|
|
8
|
+
// - the `CRTR_BROKER_ENGINE` test seam (T11) has a single dynamic-import
|
|
9
|
+
// indirection point (`loadBrokerEngine`) to swap in the fake engine.
|
|
10
|
+
//
|
|
11
|
+
// Nothing else in the tree imports this yet; it compiles but stays inert so the
|
|
12
|
+
// lifecycle suite stays green until the broker is wired up.
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, } from '@earendil-works/pi-coding-agent';
|
|
15
|
+
// Static re-exports — the real SDK surface the broker drives in production.
|
|
16
|
+
export { createAgentSessionServices, createAgentSessionFromServices, createAgentSessionRuntime, SessionManager, VERSION, };
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the broker's engine module. Defaults to the real SDK; honors the
|
|
19
|
+
* `CRTR_BROKER_ENGINE` env var (T11 test seam) so a lifecycle test can point the
|
|
20
|
+
* broker at `fixtures/fake-engine.ts` without touching production code. This is the
|
|
21
|
+
* sole dynamic-import indirection point — keep all engine resolution flowing
|
|
22
|
+
* through here so the seam stays a single, auditable swap.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadBrokerEngine() {
|
|
25
|
+
const spec = process.env.CRTR_BROKER_ENGINE ?? '@earendil-works/pi-coding-agent';
|
|
26
|
+
const mod = (await import(spec));
|
|
27
|
+
if (typeof mod.createAgentSessionServices !== 'function' ||
|
|
28
|
+
typeof mod.createAgentSessionFromServices !== 'function' ||
|
|
29
|
+
typeof mod.SessionManager !== 'function') {
|
|
30
|
+
throw new Error(`[broker] engine '${spec}' does not export createAgentSessionServices/` +
|
|
31
|
+
`createAgentSessionFromServices/SessionManager — not a valid pi-SDK-compatible engine`);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
createAgentSessionServices: mod.createAgentSessionServices,
|
|
35
|
+
createAgentSessionFromServices: mod.createAgentSessionFromServices,
|
|
36
|
+
// Optional — passed through only when the engine exposes it (real SDK yes,
|
|
37
|
+
// fake-engine no). The broker checks for its presence before the three
|
|
38
|
+
// session-replacing ops.
|
|
39
|
+
createAgentSessionRuntime: typeof mod.createAgentSessionRuntime === 'function' ? mod.createAgentSessionRuntime : undefined,
|
|
40
|
+
SessionManager: mod.SessionManager,
|
|
41
|
+
VERSION: typeof mod.VERSION === 'string' ? mod.VERSION : 'unknown',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Boot-time tripwire: compare the imported SDK `VERSION` against the `pi` binary
|
|
46
|
+
* crouter forks elsewhere (`pi --version`). On mismatch (or if the binary can't be
|
|
47
|
+
* probed) it logs a LOUD warning to stderr and proceeds — it NEVER throws. The v3
|
|
48
|
+
* session format auto-migrates on load, so minor skew round-trips; a hard fail here
|
|
49
|
+
* would risk a grace-revive crash loop. Phase 5 may harden this to fail-fast once
|
|
50
|
+
* the broker is the only host.
|
|
51
|
+
*
|
|
52
|
+
* @param engineVersion the version actually loaded (defaults to the statically
|
|
53
|
+
* imported `VERSION`; pass `engine.VERSION` from `loadBrokerEngine` to assert the
|
|
54
|
+
* version the broker is really driving).
|
|
55
|
+
*/
|
|
56
|
+
export function assertEngineVersion(engineVersion = VERSION) {
|
|
57
|
+
let binaryVersion;
|
|
58
|
+
try {
|
|
59
|
+
binaryVersion = execFileSync('pi', ['--version'], { encoding: 'utf8' }).trim();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
process.stderr.write(`[broker] WARNING: could not run 'pi --version' to verify engine parity ` +
|
|
63
|
+
`(SDK ${engineVersion}): ${err.message}\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (binaryVersion !== engineVersion) {
|
|
67
|
+
process.stderr.write(`[broker] WARNING: pi SDK version mismatch — imported SDK is ${engineVersion} but ` +
|
|
68
|
+
`the 'pi' binary is ${binaryVersion}. The v3 session format auto-migrates on load so ` +
|
|
69
|
+
`minor skew round-trips, but pin '@earendil-works/pi-coding-agent' to the binary version ` +
|
|
70
|
+
`to keep the in-process engine and the forked binary in lockstep.\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type Socket } from 'node:net';
|
|
2
|
+
import { type AgentSessionRuntime, type AgentSessionServices, type CreateAgentSessionResult, type ExtensionUIContext } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { type BrokerSdkConfig } from './launch.js';
|
|
4
|
+
import { type BrokerEngine } from './broker-sdk.js';
|
|
5
|
+
import { FrameDecoder, type ClientRole, type RpcExtensionUIRequest, type RpcExtensionUIResponse } from './broker-protocol.js';
|
|
6
|
+
interface BrokerClient {
|
|
7
|
+
id: string;
|
|
8
|
+
role: ClientRole;
|
|
9
|
+
socket: Socket;
|
|
10
|
+
decoder: FrameDecoder;
|
|
11
|
+
helloed: boolean;
|
|
12
|
+
/** Unflushed outbound bytes handed to `socket.write` but not yet flushed to the
|
|
13
|
+
* OS (M1 backpressure accounting). Incremented before each write, decremented
|
|
14
|
+
* in that write's completion callback. */
|
|
15
|
+
pendingBytes: number;
|
|
16
|
+
/** Outbound frames written but not yet flushed (the queue-depth half of the
|
|
17
|
+
* high-water mark). */
|
|
18
|
+
queuedFrames: number;
|
|
19
|
+
}
|
|
20
|
+
/** A blocking dialog awaiting the controller's response, the broker-side default
|
|
21
|
+
* timeout, or the engine's abort. */
|
|
22
|
+
interface PendingDialog {
|
|
23
|
+
/** The original request (T4) — retained so `welcome.pending_dialog` and the
|
|
24
|
+
* re-route-on-become-controller path can re-deliver a still-pending dialog to
|
|
25
|
+
* a (new) controller. The Wave-0 shape stored only the resolver. */
|
|
26
|
+
request: RpcExtensionUIRequest;
|
|
27
|
+
/** The controller answered — resolve with its parsed response (also clears the
|
|
28
|
+
* broker-side timeout and removes the entry from the registry). */
|
|
29
|
+
resolve: (response: RpcExtensionUIResponse) => void;
|
|
30
|
+
}
|
|
31
|
+
/** Dispose the live engine session if one exists (idempotent). Called by
|
|
32
|
+
* broker-cli's fatal handlers before exit so a crash never orphans detached
|
|
33
|
+
* bash children. No-op before the session is built or after a clean dispose. */
|
|
34
|
+
export declare function disposeActiveSession(): void;
|
|
35
|
+
export declare function runBroker(nodeId: string): Promise<void>;
|
|
36
|
+
export declare function buildBrokerSession(engine: BrokerEngine, cfg: BrokerSdkConfig): Promise<{
|
|
37
|
+
session: CreateAgentSessionResult['session'];
|
|
38
|
+
services: AgentSessionServices;
|
|
39
|
+
resuming: boolean;
|
|
40
|
+
/** The session-replacement runtime, present iff the engine exposes
|
|
41
|
+
* createAgentSessionRuntime (real SDK yes, fake-engine no). The broker wires
|
|
42
|
+
* its new_session/switch_session/fork ops + rebind through it. */
|
|
43
|
+
runtime?: AgentSessionRuntime;
|
|
44
|
+
}>;
|
|
45
|
+
/** Broker-side hooks the UI context needs to route (or noOp) extension dialogs. */
|
|
46
|
+
export interface BrokerDialogDeps {
|
|
47
|
+
/** The controller client, or null when ZERO viewers are attached. */
|
|
48
|
+
controller: () => BrokerClient | null;
|
|
49
|
+
/** Forward a dialog request to the (non-null) controller. */
|
|
50
|
+
forward: (client: BrokerClient, request: RpcExtensionUIRequest) => void;
|
|
51
|
+
/** Pending-dialog registry, keyed by request id (answered via extension_ui_response). */
|
|
52
|
+
pending: Map<string, PendingDialog>;
|
|
53
|
+
}
|
|
54
|
+
export declare function makeBrokerUiContext(deps: BrokerDialogDeps): ExtensionUIContext;
|
|
55
|
+
export {};
|