pi-sessions 0.7.2 → 0.9.0
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/README.md +71 -37
- package/extensions/pi-sessions.ts +125 -0
- package/extensions/session-ask/agent.ts +34 -14
- package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
- package/extensions/session-ask/navigate.ts +3 -1
- package/extensions/session-ask/presenter.ts +23 -0
- package/extensions/session-ask/renderer.ts +63 -0
- package/extensions/session-ask/tool-contract.ts +27 -0
- package/extensions/session-ask/view-model.ts +18 -0
- package/extensions/session-auto-title/generate.ts +18 -18
- package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +67 -46
- package/extensions/session-auto-title/model.ts +25 -17
- package/extensions/session-auto-title/retitle.ts +26 -12
- package/extensions/session-auto-title/wizard.ts +12 -6
- package/extensions/session-handoff/board-view-model.ts +181 -0
- package/extensions/session-handoff/board.ts +663 -0
- package/extensions/session-handoff/bootstrap.ts +174 -0
- package/extensions/session-handoff/extract.ts +101 -122
- package/extensions/session-handoff/install.ts +225 -0
- package/extensions/session-handoff/kickoff.ts +92 -0
- package/extensions/session-handoff/launch/backend.ts +16 -0
- package/extensions/session-handoff/launch/deferred.ts +20 -0
- package/extensions/session-handoff/launch/ghostty.ts +99 -0
- package/extensions/session-handoff/launch/resolution.ts +57 -0
- package/extensions/session-handoff/launch/shell.ts +7 -0
- package/extensions/session-handoff/launch/tmux.ts +31 -0
- package/extensions/session-handoff/launch-options.ts +61 -0
- package/extensions/session-handoff/launch-target.ts +91 -0
- package/extensions/session-handoff/metadata.ts +152 -63
- package/extensions/session-handoff/model.ts +58 -0
- package/extensions/session-handoff/query.ts +0 -1
- package/extensions/session-handoff/receipt.ts +96 -0
- package/extensions/session-handoff/review.ts +2 -28
- package/extensions/session-handoff/spawn.ts +76 -181
- package/extensions/session-handoff/strong-modal.ts +13 -0
- package/extensions/session-handoff/tool-contract.ts +12 -0
- package/extensions/session-handoff/tool-presenter.ts +35 -0
- package/extensions/session-handoff/tool-renderer.ts +66 -0
- package/extensions/session-handoff/tool-schema.ts +54 -0
- package/extensions/session-handoff/tool-view-model.ts +49 -0
- package/extensions/session-handoff/tool.ts +206 -0
- package/extensions/session-handoff/ui.ts +66 -0
- package/extensions/session-hooks/install.ts +75 -0
- package/extensions/{session-index.ts → session-index/install.ts} +9 -7
- package/extensions/session-messaging/broker/process.ts +21 -28
- package/extensions/session-messaging/install.ts +78 -0
- package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
- package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
- package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
- package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
- package/extensions/session-messaging/pi/message-contracts.ts +32 -0
- package/extensions/session-messaging/pi/renderer.ts +19 -12
- package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
- package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
- package/extensions/session-messaging/pi/service.ts +262 -81
- package/extensions/session-messaging/pi/tools.ts +184 -40
- package/extensions/session-search/extract.ts +60 -14
- package/extensions/session-search/hooks.ts +10 -13
- package/extensions/session-search/install.ts +373 -0
- package/extensions/session-search/renderer.ts +195 -0
- package/extensions/session-search/tool-contract.ts +51 -0
- package/extensions/shared/composition.ts +20 -0
- package/extensions/shared/errors.ts +3 -0
- package/extensions/shared/model-resolution.ts +73 -0
- package/extensions/shared/model-runtime.ts +33 -0
- package/extensions/shared/model.ts +7 -10
- package/extensions/shared/rendering/collapsible-text.ts +89 -0
- package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
- package/extensions/shared/rendering/theme.ts +5 -0
- package/extensions/shared/session-broker/client.ts +30 -29
- package/extensions/shared/session-broker/protocol.ts +128 -10
- package/extensions/shared/session-index/common.ts +5 -5
- package/extensions/shared/session-index/lineage.ts +0 -3
- package/extensions/shared/session-index/schema.ts +0 -1
- package/extensions/shared/session-index/scoring.ts +1 -2
- package/extensions/shared/session-index/search.ts +11 -4
- package/extensions/shared/session-index/store.ts +4 -9
- package/extensions/shared/settings.ts +80 -38
- package/extensions/shared/text.ts +5 -0
- package/extensions/shared/thinking-levels.ts +22 -0
- package/extensions/shared/tmux.ts +205 -0
- package/extensions/subagents/cancel.ts +111 -0
- package/extensions/subagents/classify.ts +52 -0
- package/extensions/subagents/install.ts +344 -0
- package/extensions/subagents/launch-target.ts +75 -0
- package/extensions/subagents/ledger.ts +217 -0
- package/extensions/subagents/reconcile.ts +386 -0
- package/extensions/subagents/report-message-presenter.ts +36 -0
- package/extensions/subagents/report-message-renderer.ts +21 -0
- package/extensions/subagents/report-message-view-model.ts +29 -0
- package/extensions/subagents/report.ts +159 -0
- package/extensions/subagents/roster.ts +262 -0
- package/extensions/subagents/wake.ts +173 -0
- package/images/handoff-board-subagents.png +0 -0
- package/images/handoff-board-user-sessions.png +0 -0
- package/images/handoff-subagent-report.png +0 -0
- package/images/session-handoff-tool.png +0 -0
- package/package.json +10 -16
- package/extensions/session-handoff.ts +0 -741
- package/extensions/session-hooks.ts +0 -75
- package/extensions/session-messaging/pi/message-view.ts +0 -131
- package/extensions/session-messaging.ts +0 -30
- package/extensions/session-search.ts +0 -419
- package/extensions/shared/session-broker/active.ts +0 -26
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { modelsAreEqual } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { resolveCliModel } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
export type AuthenticatedModelResolution =
|
|
8
|
+
| {
|
|
9
|
+
ok: true;
|
|
10
|
+
model: Model<Api>;
|
|
11
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
12
|
+
warning: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
ok: false;
|
|
16
|
+
error: string;
|
|
17
|
+
warning: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a model pattern with Pi's CLI grammar (fuzzy aliases, slash ids,
|
|
22
|
+
* `:thinking` suffixes), then narrows the result to a fresh entry from the
|
|
23
|
+
* runtime's available snapshot. resolveCliModel intentionally searches every
|
|
24
|
+
* model for CLI setup flows; nested extension work must only run activated,
|
|
25
|
+
* authenticated models, and the available object crosses that boundary.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveAuthenticatedModel(options: {
|
|
28
|
+
modelRuntime: ModelRuntime;
|
|
29
|
+
modelPattern: string;
|
|
30
|
+
thinkingLevel?: ThinkingLevel | undefined;
|
|
31
|
+
}): AuthenticatedModelResolution {
|
|
32
|
+
const resolved = resolveCliModel({
|
|
33
|
+
cliModel: options.modelPattern,
|
|
34
|
+
...(options.thinkingLevel ? { cliThinking: options.thinkingLevel } : {}),
|
|
35
|
+
modelRuntime: options.modelRuntime,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (resolved.error || !resolved.model) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: stripCliGuidance(resolved.error) ?? `Unknown model "${options.modelPattern}".`,
|
|
42
|
+
warning: resolved.warning,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const available = options.modelRuntime
|
|
47
|
+
.getAvailableSnapshot()
|
|
48
|
+
.find((model) => modelsAreEqual(model, resolved.model));
|
|
49
|
+
if (!available) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: `Model "${resolved.model.provider}/${resolved.model.id}" is not an available authenticated model.`,
|
|
53
|
+
warning: resolved.warning,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
model: available,
|
|
60
|
+
thinkingLevel: options.thinkingLevel ?? resolved.thinkingLevel,
|
|
61
|
+
warning: resolved.warning,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// resolveCliModel errors reference --list-models, which does not exist in extension surfaces.
|
|
66
|
+
function stripCliGuidance(error: string | undefined): string | undefined {
|
|
67
|
+
const stripped = error?.replace(/(?:^|\s+)[^.!?\n]*--list-models[^.!?\n]*(?:[.!?]|$)/, "").trim();
|
|
68
|
+
return stripped || undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatAvailableModelList(models: readonly Model<Api>[]): string {
|
|
72
|
+
return models.map((model) => `${model.provider}/${model.id}`).join(", ");
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
export type ModelRuntimeProvider = (modelRegistry: ModelRegistry) => Promise<ModelRuntime>;
|
|
5
|
+
|
|
6
|
+
// Workaround for a missing extension API: resolveCliModel and createAgentSession want a
|
|
7
|
+
// ModelRuntime, but ExtensionContext only exposes the ModelRegistry compatibility facade,
|
|
8
|
+
// which keeps its runtime private. Until pi exposes the runtime to extensions, build a
|
|
9
|
+
// fresh one per use and mirror the registered providers (e.g. pi-claude-bridge) so their
|
|
10
|
+
// configs — including live streamSimple closures — carry over by reference.
|
|
11
|
+
export async function createSessionModelRuntime(
|
|
12
|
+
modelRegistry: ModelRegistry,
|
|
13
|
+
): Promise<ModelRuntime> {
|
|
14
|
+
const modelRuntime = await ModelRuntime.create({ allowModelNetwork: false });
|
|
15
|
+
|
|
16
|
+
for (const providerId of modelRegistry.getRegisteredProviderIds()) {
|
|
17
|
+
const config = modelRegistry.getRegisteredProviderConfig(providerId);
|
|
18
|
+
if (config) {
|
|
19
|
+
modelRuntime.registerProvider(providerId, config);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
24
|
+
return modelRuntime;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A model reference carried on the session context is stale relative to the mirrored runtime
|
|
28
|
+
// (it was resolved against the facade's private runtime, not this one). Re-resolve it against
|
|
29
|
+
// the runtime's snapshot so downstream calls use the mirror's live provider config, falling
|
|
30
|
+
// back to the original reference when the runtime has not seen it.
|
|
31
|
+
export function freshenModel(runtime: ModelRuntime, model: Model<Api>): Model<Api> {
|
|
32
|
+
return runtime.getModel(model.provider, model.id) ?? model;
|
|
33
|
+
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
export interface ExactModelReference {
|
|
4
|
+
provider: string;
|
|
5
|
+
modelId: string;
|
|
6
|
+
}
|
|
3
7
|
|
|
4
8
|
export function findModelByReference(
|
|
5
|
-
availableModels: Model<Api>[],
|
|
6
|
-
reference:
|
|
9
|
+
availableModels: readonly Model<Api>[],
|
|
10
|
+
reference: ExactModelReference,
|
|
7
11
|
): Model<Api> | undefined {
|
|
8
12
|
return availableModels.find(
|
|
9
13
|
(model) => model.provider === reference.provider && model.id === reference.modelId,
|
|
10
14
|
);
|
|
11
15
|
}
|
|
12
|
-
|
|
13
|
-
export function resolveConfiguredModel(
|
|
14
|
-
availableModels: Model<Api>[],
|
|
15
|
-
configuredModel: ModelReference | undefined,
|
|
16
|
-
): Model<Api> | undefined {
|
|
17
|
-
return configuredModel ? findModelByReference(availableModels, configuredModel) : undefined;
|
|
18
|
-
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
getKeybindings,
|
|
4
|
+
visibleWidth,
|
|
5
|
+
wrapTextWithAnsi,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
import type { RenderTheme } from "./theme.ts";
|
|
8
|
+
|
|
9
|
+
interface CollapsibleTextKeybindings {
|
|
10
|
+
getKeys(keybinding: "app.tools.expand"): string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CollapsibleTextOptions {
|
|
14
|
+
collapsedRows: number;
|
|
15
|
+
theme: RenderTheme;
|
|
16
|
+
keybindings?: CollapsibleTextKeybindings | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class CollapsibleText implements Component {
|
|
20
|
+
private text = "";
|
|
21
|
+
private expanded = false;
|
|
22
|
+
private visibleSupplementalRows = 0;
|
|
23
|
+
private hiddenSupplementalRows = 0;
|
|
24
|
+
private readonly collapsedRows: number;
|
|
25
|
+
private readonly theme: RenderTheme;
|
|
26
|
+
private readonly keybindings: CollapsibleTextKeybindings;
|
|
27
|
+
|
|
28
|
+
constructor(options: CollapsibleTextOptions) {
|
|
29
|
+
this.collapsedRows = Math.max(1, Math.floor(options.collapsedRows));
|
|
30
|
+
this.theme = options.theme;
|
|
31
|
+
this.keybindings = options.keybindings ?? getKeybindings();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setText(text: string): void {
|
|
35
|
+
this.text = text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setExpanded(expanded: boolean): void {
|
|
39
|
+
this.expanded = expanded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setSupplementalRowCounts(visibleRows: number, hiddenRows: number): void {
|
|
43
|
+
this.visibleSupplementalRows = Math.max(0, Math.floor(visibleRows));
|
|
44
|
+
this.hiddenSupplementalRows = Math.max(0, Math.floor(hiddenRows));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
invalidate(): void {}
|
|
48
|
+
|
|
49
|
+
render(width: number): string[] {
|
|
50
|
+
const renderWidth = Math.max(1, width);
|
|
51
|
+
const rows = wrapTextWithAnsi(this.text, renderWidth);
|
|
52
|
+
if (this.expanded) {
|
|
53
|
+
return rows.map((row) => padToWidth(row, renderWidth));
|
|
54
|
+
}
|
|
55
|
+
const hiddenBodyRows = Math.max(0, rows.length - this.collapsedRows);
|
|
56
|
+
if (hiddenBodyRows === 0 && this.hiddenSupplementalRows === 0) {
|
|
57
|
+
return rows.map((row) => padToWidth(row, renderWidth));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const remaining = hiddenBodyRows + this.hiddenSupplementalRows;
|
|
61
|
+
const total = rows.length + this.visibleSupplementalRows + this.hiddenSupplementalRows;
|
|
62
|
+
const visibleRows = rows
|
|
63
|
+
.slice(0, this.collapsedRows)
|
|
64
|
+
.map((row) => padToWidth(row, renderWidth));
|
|
65
|
+
const hintRows = wrapTextWithAnsi(this.formatExpandHint(remaining, total), renderWidth).map(
|
|
66
|
+
(row) => padToWidth(row, renderWidth),
|
|
67
|
+
);
|
|
68
|
+
return [...visibleRows, ...hintRows];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private formatExpandHint(remaining: number, total: number): string {
|
|
72
|
+
const keys = this.keybindings.getKeys("app.tools.expand").join("/");
|
|
73
|
+
const lineLabel = remaining === 1 ? "line" : "lines";
|
|
74
|
+
if (!keys) {
|
|
75
|
+
return this.theme.fg(
|
|
76
|
+
"muted",
|
|
77
|
+
`(${remaining} more ${lineLabel}, ${total} total, expand key unbound)`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return `${this.theme.fg("muted", `(${remaining} more ${lineLabel}, ${total} total, `)}${this.theme.fg(
|
|
81
|
+
"dim",
|
|
82
|
+
keys,
|
|
83
|
+
)}${this.theme.fg("muted", " to expand)")}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function padToWidth(text: string, width: number): string {
|
|
88
|
+
return `${text}${" ".repeat(Math.max(0, width - visibleWidth(text)))}`;
|
|
89
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type Component, Container, Spacer, Text, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
import { CollapsibleText } from "./collapsible-text.ts";
|
|
3
|
+
import type { RenderTheme } from "./theme.ts";
|
|
4
|
+
|
|
5
|
+
export interface ExpandableContentPresentation {
|
|
6
|
+
header: string;
|
|
7
|
+
metadata?: string[] | undefined;
|
|
8
|
+
expandedMetadata?: string[] | undefined;
|
|
9
|
+
body?:
|
|
10
|
+
| {
|
|
11
|
+
text: string;
|
|
12
|
+
collapsedRows: number;
|
|
13
|
+
spacingBefore?: number | undefined;
|
|
14
|
+
}
|
|
15
|
+
| undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ExpandableContentLayout implements Component {
|
|
19
|
+
private presentation: ExpandableContentPresentation | undefined;
|
|
20
|
+
private expanded = false;
|
|
21
|
+
private body: CollapsibleText | undefined;
|
|
22
|
+
private bodyRows: number | undefined;
|
|
23
|
+
|
|
24
|
+
constructor(private readonly theme: RenderTheme) {}
|
|
25
|
+
|
|
26
|
+
update(presentation: ExpandableContentPresentation, expanded: boolean): void {
|
|
27
|
+
this.presentation = presentation;
|
|
28
|
+
this.expanded = expanded;
|
|
29
|
+
|
|
30
|
+
if (!presentation.body) {
|
|
31
|
+
this.body = undefined;
|
|
32
|
+
this.bodyRows = undefined;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!this.body || this.bodyRows !== presentation.body.collapsedRows) {
|
|
37
|
+
this.body = new CollapsibleText({
|
|
38
|
+
collapsedRows: presentation.body.collapsedRows,
|
|
39
|
+
theme: this.theme,
|
|
40
|
+
});
|
|
41
|
+
this.bodyRows = presentation.body.collapsedRows;
|
|
42
|
+
}
|
|
43
|
+
this.body.setText(presentation.body.text);
|
|
44
|
+
this.body.setExpanded(expanded);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
invalidate(): void {
|
|
48
|
+
this.body?.invalidate();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(width: number): string[] {
|
|
52
|
+
if (!this.presentation) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const container = new Container();
|
|
57
|
+
container.addChild(new Text(this.presentation.header, 0, 0));
|
|
58
|
+
|
|
59
|
+
const visibleMetadata = this.presentation.metadata ?? [];
|
|
60
|
+
const expandedMetadata = this.presentation.expandedMetadata ?? [];
|
|
61
|
+
const metadata = [...visibleMetadata, ...(this.expanded ? expandedMetadata : [])];
|
|
62
|
+
if (metadata.length > 0) {
|
|
63
|
+
container.addChild(new Text(metadata.join("\n"), 0, 0));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (this.body && this.presentation.body?.text) {
|
|
67
|
+
this.body.setSupplementalRowCounts(
|
|
68
|
+
countRenderedRows(visibleMetadata, width),
|
|
69
|
+
countRenderedRows(expandedMetadata, width),
|
|
70
|
+
);
|
|
71
|
+
const spacingBefore = this.presentation.body.spacingBefore ?? 1;
|
|
72
|
+
if (spacingBefore > 0) {
|
|
73
|
+
container.addChild(new Spacer(spacingBefore));
|
|
74
|
+
}
|
|
75
|
+
container.addChild(this.body);
|
|
76
|
+
}
|
|
77
|
+
return container.render(width);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function countRenderedRows(lines: readonly string[], width: number): number {
|
|
82
|
+
const renderWidth = Math.max(1, width);
|
|
83
|
+
return lines.reduce((total, line) => total + wrapTextWithAnsi(line, renderWidth).length, 0);
|
|
84
|
+
}
|
|
@@ -2,10 +2,15 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { readFrames, writeFrame } from "./framing.ts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
BROKER_FRAME_SCHEMA,
|
|
7
|
+
type OutboundSessionEnvelope,
|
|
8
|
+
type SessionEnvelopeSendFailureReason,
|
|
9
|
+
type SessionMessagingBrokerFrame,
|
|
10
|
+
} from "./protocol.ts";
|
|
6
11
|
import { getSessionMessagingSocketPath } from "./socket-path.ts";
|
|
7
12
|
|
|
8
|
-
export const
|
|
13
|
+
export const INCOMING_SESSION_ENVELOPE_EVENT = "session_messaging.incoming";
|
|
9
14
|
|
|
10
15
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
11
16
|
const SOCKET_PATH = getSessionMessagingSocketPath();
|
|
@@ -17,24 +22,22 @@ interface PendingListRequest {
|
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
interface PendingSendRequest {
|
|
20
|
-
resolve(result:
|
|
25
|
+
resolve(result: SessionEnvelopeSendResult): void;
|
|
21
26
|
reject(error: Error): void;
|
|
22
27
|
timeout: NodeJS.Timeout;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
export type SessionEnvelopeSendResult =
|
|
31
|
+
| { delivered: true }
|
|
32
|
+
| {
|
|
33
|
+
delivered: false;
|
|
34
|
+
reason?: SessionEnvelopeSendFailureReason | undefined;
|
|
35
|
+
error?: string | undefined;
|
|
36
|
+
};
|
|
30
37
|
|
|
31
|
-
export interface
|
|
32
|
-
messageId: string;
|
|
38
|
+
export interface SendSessionEnvelopeOptions {
|
|
33
39
|
target: string;
|
|
34
|
-
|
|
35
|
-
sentAt: string;
|
|
36
|
-
requestResponse?: boolean | undefined;
|
|
37
|
-
sourceToolCallId?: string | undefined;
|
|
40
|
+
envelope: OutboundSessionEnvelope;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export class SessionMessagingClient extends EventEmitter {
|
|
@@ -139,39 +142,33 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
139
142
|
});
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
sendEnvelope(options: SendSessionEnvelopeOptions): Promise<SessionEnvelopeSendResult> {
|
|
143
146
|
const socket = this.requireSocket();
|
|
144
147
|
const requestId = randomUUID();
|
|
145
148
|
|
|
146
149
|
return new Promise((resolve, reject) => {
|
|
147
150
|
const timeout = setTimeout(() => {
|
|
148
151
|
this.pendingSends.delete(requestId);
|
|
149
|
-
reject(new Error("Session
|
|
152
|
+
reject(new Error("Session envelope send timed out."));
|
|
150
153
|
}, REQUEST_TIMEOUT_MS + 1_000);
|
|
151
154
|
this.pendingSends.set(requestId, { resolve, reject, timeout });
|
|
152
155
|
writeFrame(socket, {
|
|
153
156
|
type: "send",
|
|
154
157
|
requestId,
|
|
155
|
-
messageId: options.messageId,
|
|
156
158
|
target: options.target,
|
|
157
|
-
|
|
158
|
-
requestResponse: options.requestResponse,
|
|
159
|
-
sourceToolCallId: options.sourceToolCallId,
|
|
160
|
-
sentAt: options.sentAt,
|
|
159
|
+
envelope: options.envelope,
|
|
161
160
|
});
|
|
162
161
|
});
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
acknowledgeIncoming(
|
|
166
165
|
requestId: string,
|
|
167
|
-
messageId: string,
|
|
168
166
|
result: { delivered: true } | { delivered: false; error: string },
|
|
169
167
|
): void {
|
|
170
168
|
const socket = this.requireSocket();
|
|
171
169
|
writeFrame(socket, {
|
|
172
170
|
type: "incoming_ack",
|
|
173
171
|
requestId,
|
|
174
|
-
messageId,
|
|
175
172
|
delivered: result.delivered,
|
|
176
173
|
error: result.delivered ? undefined : result.error,
|
|
177
174
|
});
|
|
@@ -209,18 +206,22 @@ export class SessionMessagingClient extends EventEmitter {
|
|
|
209
206
|
break;
|
|
210
207
|
}
|
|
211
208
|
case "incoming":
|
|
212
|
-
this.emit(
|
|
209
|
+
this.emit(INCOMING_SESSION_ENVELOPE_EVENT, frame.requestId, frame.envelope);
|
|
213
210
|
break;
|
|
214
211
|
case "send_result": {
|
|
215
212
|
const pending = this.pendingSends.get(frame.requestId);
|
|
216
213
|
if (!pending) return;
|
|
217
214
|
clearTimeout(pending.timeout);
|
|
218
215
|
this.pendingSends.delete(frame.requestId);
|
|
219
|
-
pending.resolve(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
216
|
+
pending.resolve(
|
|
217
|
+
frame.delivered
|
|
218
|
+
? { delivered: true }
|
|
219
|
+
: {
|
|
220
|
+
delivered: false,
|
|
221
|
+
...(frame.reason === undefined ? {} : { reason: frame.reason }),
|
|
222
|
+
...(frame.error === undefined ? {} : { error: frame.error }),
|
|
223
|
+
},
|
|
224
|
+
);
|
|
224
225
|
break;
|
|
225
226
|
}
|
|
226
227
|
case "error":
|
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
import { type Static, Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const TASK_REPORT_REFERENCE_SCHEMA = Type.Object({
|
|
4
|
+
reference: Type.String({
|
|
5
|
+
description:
|
|
6
|
+
"A precise reference such as a file path and line range, symbol name, command, URL, session id, or external artifact.",
|
|
7
|
+
}),
|
|
8
|
+
description: Type.Optional(
|
|
9
|
+
Type.String({
|
|
10
|
+
description: "Why this reference matters or what evidence the parent should take from it.",
|
|
11
|
+
}),
|
|
12
|
+
),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const TASK_REPORT_SCHEMA = Type.Object({
|
|
16
|
+
status: Type.Union(
|
|
17
|
+
[
|
|
18
|
+
Type.Literal("done", {
|
|
19
|
+
description: "The delegated task or requested follow-up is complete.",
|
|
20
|
+
}),
|
|
21
|
+
Type.Literal("blocked", {
|
|
22
|
+
description:
|
|
23
|
+
"The task cannot continue without a specific decision, clarification, permission, dependency, or other action outside the scope of the task.",
|
|
24
|
+
}),
|
|
25
|
+
Type.Literal("incomplete", {
|
|
26
|
+
description:
|
|
27
|
+
"Useful work was performed, but the requested result could not be completed and it's uncertain what should be done to reach completion.",
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
{ description: "The subagent's assessment of the task at the end of this turn." },
|
|
31
|
+
),
|
|
32
|
+
summary: Type.String({
|
|
33
|
+
description:
|
|
34
|
+
"A concise, 3-4 sentence self-contained account of the outcome and its most important evidence.",
|
|
35
|
+
}),
|
|
36
|
+
details: Type.Optional(
|
|
37
|
+
Type.String({
|
|
38
|
+
description:
|
|
39
|
+
"Optional supporting context such as reasoning, implementation notes, validation performed, limitations, unexpected behavior or roadblocks, or failed approaches.",
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
references: Type.Optional(
|
|
43
|
+
Type.Array(TASK_REPORT_REFERENCE_SCHEMA, {
|
|
44
|
+
description: "Supporting material for the report.",
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
nextSteps: Type.Optional(
|
|
48
|
+
Type.Array(Type.String(), {
|
|
49
|
+
description:
|
|
50
|
+
"Optional concrete actions that would extend or build on the state of the work. If blocked or incomplete, call out specifically what is causing the early return.",
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const OUTBOUND_MESSAGE_ENVELOPE_SCHEMA = Type.Object({
|
|
56
|
+
kind: Type.Literal("message"),
|
|
57
|
+
messageId: Type.String(),
|
|
58
|
+
body: Type.String(),
|
|
59
|
+
requestResponse: Type.Optional(Type.Boolean()),
|
|
60
|
+
sentAt: Type.String(),
|
|
61
|
+
sourceToolCallId: Type.Optional(Type.String()),
|
|
62
|
+
});
|
|
63
|
+
const OUTBOUND_CANCEL_ENVELOPE_SCHEMA = Type.Object({
|
|
64
|
+
kind: Type.Literal("cancel"),
|
|
65
|
+
cancelId: Type.String(),
|
|
66
|
+
sentAt: Type.String(),
|
|
67
|
+
});
|
|
68
|
+
const OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA = Type.Intersect([
|
|
69
|
+
Type.Object({
|
|
70
|
+
kind: Type.Literal("subagent_report"),
|
|
71
|
+
reportId: Type.String(),
|
|
72
|
+
sentAt: Type.String(),
|
|
73
|
+
}),
|
|
74
|
+
TASK_REPORT_SCHEMA,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
export const OUTBOUND_SESSION_ENVELOPE_SCHEMA = Type.Union([
|
|
78
|
+
OUTBOUND_MESSAGE_ENVELOPE_SCHEMA,
|
|
79
|
+
OUTBOUND_CANCEL_ENVELOPE_SCHEMA,
|
|
80
|
+
OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA,
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
export const SESSION_MESSAGE_ENVELOPE_SCHEMA = Type.Object({
|
|
84
|
+
kind: Type.Literal("message"),
|
|
4
85
|
messageId: Type.String(),
|
|
5
86
|
source: Type.String(),
|
|
6
87
|
target: Type.String(),
|
|
@@ -9,6 +90,29 @@ export const SESSION_MESSAGE_PAYLOAD_SCHEMA = Type.Object({
|
|
|
9
90
|
sentAt: Type.String(),
|
|
10
91
|
sourceToolCallId: Type.Optional(Type.String()),
|
|
11
92
|
});
|
|
93
|
+
export const SESSION_CANCEL_ENVELOPE_SCHEMA = Type.Object({
|
|
94
|
+
kind: Type.Literal("cancel"),
|
|
95
|
+
cancelId: Type.String(),
|
|
96
|
+
source: Type.String(),
|
|
97
|
+
target: Type.String(),
|
|
98
|
+
sentAt: Type.String(),
|
|
99
|
+
});
|
|
100
|
+
export const SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA = Type.Intersect([
|
|
101
|
+
Type.Object({
|
|
102
|
+
kind: Type.Literal("subagent_report"),
|
|
103
|
+
reportId: Type.String(),
|
|
104
|
+
source: Type.String(),
|
|
105
|
+
target: Type.String(),
|
|
106
|
+
sentAt: Type.String(),
|
|
107
|
+
}),
|
|
108
|
+
TASK_REPORT_SCHEMA,
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
export const SESSION_ENVELOPE_SCHEMA = Type.Union([
|
|
112
|
+
SESSION_MESSAGE_ENVELOPE_SCHEMA,
|
|
113
|
+
SESSION_CANCEL_ENVELOPE_SCHEMA,
|
|
114
|
+
SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA,
|
|
115
|
+
]);
|
|
12
116
|
|
|
13
117
|
const REGISTER_CLIENT_FRAME_SCHEMA = Type.Object({
|
|
14
118
|
type: Type.Literal("register"),
|
|
@@ -24,17 +128,12 @@ const LIST_CLIENT_FRAME_SCHEMA = Type.Object({
|
|
|
24
128
|
const SEND_CLIENT_FRAME_SCHEMA = Type.Object({
|
|
25
129
|
type: Type.Literal("send"),
|
|
26
130
|
requestId: Type.String(),
|
|
27
|
-
messageId: Type.String(),
|
|
28
131
|
target: Type.String(),
|
|
29
|
-
|
|
30
|
-
requestResponse: Type.Optional(Type.Boolean()),
|
|
31
|
-
sentAt: Type.String(),
|
|
32
|
-
sourceToolCallId: Type.Optional(Type.String()),
|
|
132
|
+
envelope: OUTBOUND_SESSION_ENVELOPE_SCHEMA,
|
|
33
133
|
});
|
|
34
134
|
const INCOMING_ACK_CLIENT_FRAME_SCHEMA = Type.Object({
|
|
35
135
|
type: Type.Literal("incoming_ack"),
|
|
36
136
|
requestId: Type.String(),
|
|
37
|
-
messageId: Type.String(),
|
|
38
137
|
delivered: Type.Boolean(),
|
|
39
138
|
error: Type.Optional(Type.String()),
|
|
40
139
|
});
|
|
@@ -55,13 +154,18 @@ const SESSIONS_BROKER_FRAME_SCHEMA = Type.Object({
|
|
|
55
154
|
const INCOMING_BROKER_FRAME_SCHEMA = Type.Object({
|
|
56
155
|
type: Type.Literal("incoming"),
|
|
57
156
|
requestId: Type.String(),
|
|
58
|
-
|
|
157
|
+
envelope: SESSION_ENVELOPE_SCHEMA,
|
|
59
158
|
});
|
|
159
|
+
export const SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA = Type.Union([
|
|
160
|
+
Type.Literal("no_session"),
|
|
161
|
+
Type.Literal("disconnected"),
|
|
162
|
+
]);
|
|
163
|
+
|
|
60
164
|
const SEND_RESULT_BROKER_FRAME_SCHEMA = Type.Object({
|
|
61
165
|
type: Type.Literal("send_result"),
|
|
62
166
|
requestId: Type.String(),
|
|
63
|
-
messageId: Type.String(),
|
|
64
167
|
delivered: Type.Boolean(),
|
|
168
|
+
reason: Type.Optional(SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA),
|
|
65
169
|
error: Type.Optional(Type.String()),
|
|
66
170
|
});
|
|
67
171
|
const ERROR_BROKER_FRAME_SCHEMA = Type.Object({
|
|
@@ -86,7 +190,21 @@ export const BROKER_FRAME_SCHEMA = Type.Union([
|
|
|
86
190
|
ERROR_BROKER_FRAME_SCHEMA,
|
|
87
191
|
]);
|
|
88
192
|
|
|
89
|
-
export type
|
|
193
|
+
export type SessionEnvelopeSendFailureReason = Static<
|
|
194
|
+
typeof SESSION_ENVELOPE_SEND_FAILURE_REASON_SCHEMA
|
|
195
|
+
>;
|
|
196
|
+
export type TaskReportReference = Static<typeof TASK_REPORT_REFERENCE_SCHEMA>;
|
|
197
|
+
export type TaskReport = Static<typeof TASK_REPORT_SCHEMA>;
|
|
198
|
+
export type OutboundSessionMessageEnvelope = Static<typeof OUTBOUND_MESSAGE_ENVELOPE_SCHEMA>;
|
|
199
|
+
export type OutboundSessionCancelEnvelope = Static<typeof OUTBOUND_CANCEL_ENVELOPE_SCHEMA>;
|
|
200
|
+
export type OutboundSubagentReportEnvelope = Static<
|
|
201
|
+
typeof OUTBOUND_SUBAGENT_REPORT_ENVELOPE_SCHEMA
|
|
202
|
+
>;
|
|
203
|
+
export type OutboundSessionEnvelope = Static<typeof OUTBOUND_SESSION_ENVELOPE_SCHEMA>;
|
|
204
|
+
export type SessionMessageEnvelope = Static<typeof SESSION_MESSAGE_ENVELOPE_SCHEMA>;
|
|
205
|
+
export type SessionCancelEnvelope = Static<typeof SESSION_CANCEL_ENVELOPE_SCHEMA>;
|
|
206
|
+
export type SessionSubagentReportEnvelope = Static<typeof SESSION_SUBAGENT_REPORT_ENVELOPE_SCHEMA>;
|
|
207
|
+
export type SessionEnvelope = Static<typeof SESSION_ENVELOPE_SCHEMA>;
|
|
90
208
|
export type SessionMessagingSendClientFrame = Static<typeof SEND_CLIENT_FRAME_SCHEMA>;
|
|
91
209
|
export type SessionMessagingIncomingAckClientFrame = Static<
|
|
92
210
|
typeof INCOMING_ACK_CLIENT_FRAME_SCHEMA
|
|
@@ -3,9 +3,10 @@ import type { FileTouchOp, FileTouchSource, PathScope } from "../../session-sear
|
|
|
3
3
|
import { safeParseTypeBoxJson } from "../typebox.ts";
|
|
4
4
|
import type { SqliteDatabase } from "./sqlite.ts";
|
|
5
5
|
|
|
6
|
-
export const INDEX_SCHEMA_VERSION =
|
|
6
|
+
export const INDEX_SCHEMA_VERSION = 14;
|
|
7
7
|
|
|
8
|
-
export type SessionOrigin = "handoff" | "fork" | "unknown_child";
|
|
8
|
+
export type SessionOrigin = "handoff" | "subagent" | "fork" | "unknown_child";
|
|
9
|
+
export type SessionKind = "user" | "subagent";
|
|
9
10
|
export type SessionLineageRelation =
|
|
10
11
|
| "self"
|
|
11
12
|
| "parent"
|
|
@@ -32,7 +33,6 @@ export interface SessionRow {
|
|
|
32
33
|
parentSessionId?: string | undefined;
|
|
33
34
|
sessionOrigin?: SessionOrigin | undefined;
|
|
34
35
|
handoffGoal?: string | undefined;
|
|
35
|
-
handoffNextTask?: string | undefined;
|
|
36
36
|
indexedFileSize?: number | undefined;
|
|
37
37
|
indexedFileMtimeMs?: number | undefined;
|
|
38
38
|
indexedFileAnchor?: string | undefined;
|
|
@@ -50,7 +50,6 @@ export interface SessionLineageRow {
|
|
|
50
50
|
parentSessionId?: string | undefined;
|
|
51
51
|
sessionOrigin?: SessionOrigin | undefined;
|
|
52
52
|
handoffGoal?: string | undefined;
|
|
53
|
-
handoffNextTask?: string | undefined;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
export interface SessionRelatedSessionRow extends SessionLineageRow {
|
|
@@ -93,6 +92,7 @@ export interface SearchSessionsParams {
|
|
|
93
92
|
before?: string | undefined;
|
|
94
93
|
touched?: string[] | undefined;
|
|
95
94
|
changed?: string[] | undefined;
|
|
95
|
+
kind?: SessionKind | undefined;
|
|
96
96
|
sort?: SearchSort | undefined;
|
|
97
97
|
limit?: number | undefined;
|
|
98
98
|
excludeSessionIds?: string[] | undefined;
|
|
@@ -135,7 +135,6 @@ export interface SearchSessionResult {
|
|
|
135
135
|
firstUserPrompt?: string | undefined;
|
|
136
136
|
sessionOrigin?: SessionOrigin | undefined;
|
|
137
137
|
handoffGoal?: string | undefined;
|
|
138
|
-
handoffNextTask?: string | undefined;
|
|
139
138
|
relation?: SessionLineageRelation | undefined;
|
|
140
139
|
snippet: string;
|
|
141
140
|
evidence: SessionSearchEvidence[];
|
|
@@ -156,6 +155,7 @@ export type SessionIndexDatabase = SqliteDatabase;
|
|
|
156
155
|
export const NULLABLE_STRING_SCHEMA = Type.Union([Type.String(), Type.Null()]);
|
|
157
156
|
export const SESSION_ORIGIN_SCHEMA = Type.Union([
|
|
158
157
|
Type.Literal("handoff"),
|
|
158
|
+
Type.Literal("subagent"),
|
|
159
159
|
Type.Literal("fork"),
|
|
160
160
|
Type.Literal("unknown_child"),
|
|
161
161
|
]);
|