File size: 4,990 Bytes
bc575bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/** Service contracts and everything that crosses the wire. */

import type { HarnessEvent, LaneSnapshot } from "@earendil-works/pi-agent-core";
import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai";

/** Commands answer with data, never exceptions, exactly like a remote call would. */
export type CommandResult = { ok: true } | { ok: false; error: string };

/** Durable model identity. Nothing outside a worker holds a `Model` object. */
export interface ModelRef {
	provider: string;
	modelId: string;
}

export interface ModelSummary extends ModelRef {
	name: string;
}

export interface ProviderAccount {
	id: string;
	name: string;
	authType: "oauth" | "api_key";
	configured: boolean;
	/** Where the credential came from, for display: "stored", "environment", an env var name. */
	source?: string;
	/** False for ambient credentials pi cannot collect itself, such as AWS profiles or env vars. */
	interactive: boolean;
	methodName?: string;
}

export interface ModelsState {
	readonly models: readonly ModelSummary[];
	readonly accounts: readonly ProviderAccount[];
	readonly refreshing: boolean;
}

/** An auth prompt without its `AbortSignal`: the part that can cross a transport. */
export type AuthPromptRequest = AuthPrompt extends infer Prompt
	? Prompt extends unknown
		? Omit<Prompt, "signal">
		: never
	: never;

/** A service seen from the other side of a connection: every method returns a promise. */
export type Remote<T> = {
	[K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<Awaited<R>> : never;
};

/** Names one service and carries its call and event types. Names are globally unique. */
export interface ServiceToken<TApi extends object, TEvent = never> {
	readonly name: string;
	/** Phantom, never read: keeps the types attached to the token. */
	readonly types?: (api: TApi, event: TEvent) => void;
}

export function defineService<TApi extends object, TEvent = never>(name: string): ServiceToken<TApi, TEvent> {
	return { name };
}

export interface SessionSummary {
	id: string;
	path: string;
	cwd: string;
	createdAt: number;
}

export interface SessionSnapshot {
	sessionId: string;
	cwd: string;
	sessionPath: string;
	/** Carries the lane configuration, queues, and stats: no side-channel replication. */
	lane: LaneSnapshot;
	models: ModelsState;
}

/** Everything the `Models` service publishes. */
export type ModelsEvent =
	| { type: "state"; state: ModelsState }
	// Login runs the wrong way round: the request is an event, the answer is an ordinary call.
	| { type: "prompt"; requestId: string; request: AuthPromptRequest }
	| { type: "notice"; notice: AuthEvent };

/** The login half, for whatever drives the dialog. */
export type AuthEventPayload = Exclude<ModelsEvent, { type: "state" }>;

/** One presentation's subscription: a `lane.watch()` in the worker, named so its events can be filtered. */
export interface LaneSubscription {
	subscriptionId: string;
	snapshot: SessionSnapshot;
}

/** Lane events are addressed to the subscription whose watch produced them. */
export interface LaneEvent {
	subscriptionId: string;
	event: HarnessEvent;
}

export interface LaneServiceApi {
	/**
	 * Capture a snapshot and open a subscription for one presentation. Its events are addressed to
	 * `presentationId`, so the server routes them instead of broadcasting. Buffered until `start`.
	 */
	watch(presentationId: string): Promise<LaneSubscription>;
	/** Begin delivery, draining everything buffered since the snapshot. */
	start(subscriptionId: string): Promise<void>;
	unwatch(subscriptionId: string): Promise<void>;
	prompt(text: string): Promise<CommandResult>;
	steer(text: string): Promise<CommandResult>;
	followUp(text: string): Promise<CommandResult>;
	compact(): Promise<CommandResult>;
	abort(): Promise<CommandResult>;
	setModel(ref: ModelRef): Promise<CommandResult>;
}

export interface ModelsServiceApi {
	refresh(): Promise<CommandResult>;
	login(providerId: string, authType: "oauth" | "api_key"): Promise<CommandResult>;
	authReply(requestId: string, answer: string | null): Promise<void>;
}

/** Provided by a worker so the server can identify the session it opened, without naming lane methods. */
export interface WorkerServiceApi {
	describe(): Promise<{ sessionId: string }>;
}

export interface SessionsServiceApi {
	list(): Promise<SessionSummary[]>;
	attach(sessionId: string | null, cwd: string, presentationId: string): Promise<string>;
}

/** Provided by the worker. One subscription per presentation; `watch` again to rebase. */
export const Lane = defineService<LaneServiceApi, LaneEvent>("lane");
/** Provided by the worker. Small enough to publish whole. */
export const Models = defineService<ModelsServiceApi, ModelsEvent>("models");
/** Provided by the worker, consumed only by the server. */
export const Worker = defineService<WorkerServiceApi>("worker");
/** Provided by the server. */
export const Sessions = defineService<SessionsServiceApi>("sessions");