Repository navigation
Expand file tree
/
Copy pathadapter.ts
More file actions
199 lines (184 loc) · 9.08 KB
/
Copy pathadapter.ts
File metadata and controls
199 lines (184 loc) · 9.08 KB
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// The contract between the core and a coding agent. The core never names an
// agent: its events, launch flags, delivery and wording all come through here.
import type { EventEmitter } from 'node:events';
import type { WebSocket } from 'ws';
import type { AgentObserver } from './agent-observer.ts';
/** Wording and capabilities the core uses for this agent. */
export interface AgentProfile {
/** Command name, and the prefix of the turn-state key in context records (`<id>_turn_state`). */
id: string;
/** Short name used in sentences: "Refer to that agent as <name>." */
name: string;
/** Product name: "Your request has been sent to <product>." */
product: string;
/** The event that ends the agent's turn. */
turnEnd: string;
/** The event that records a prompt the agent received. */
promptEvent: string;
/** Assistant text arrives while a turn runs, not only when it ends. */
streamsMessages: boolean;
/** A request can reach the agent during a running turn. */
canSteer: boolean;
/** One observed event, and several: "every <name> <eventWord>", "<name> <eventsWord>". */
eventWord: string;
eventsWord: string;
/** How requests reach the agent: "Delivered to <transport>". */
transport: string;
/** The message a request becomes on the way: "<Wire> JSON" in the request inspector. */
wire: string;
}
/** What an observation is, in terms every agent shares. The feed decides by kind. */
export type ObservationKind =
| 'prompt' // a prompt the agent received
| 'text' // conversation text, from the agent or the operator
| 'tool' // a tool call or its result
| 'tool_failure' // a failed tool call
| 'attention' // an approval, question or denial involving the operator
| 'turn_end' // the agent finished its response
| 'turn_failed' // the turn ended in an error
| 'task_completed' // a tracked task was completed
| 'session_end' // the agent session ended
| 'event'; // anything else
/** The agent's turn as recorded in context sent to Live. */
export type TurnState = 'unknown' | 'working' | 'turn_finished' | 'needs_operator' | 'failed' | 'exited';
/** The agent's state as shown to the operator. */
export type AgentState = 'starting' | 'idle' | 'working' | 'needs_attention' | 'failed' | 'exited';
/** One observed agent event, in the format shared by all adapters. */
export interface Observation {
kind: ObservationKind;
/** The agent's own name for the event. */
name: string;
/** The complete event as redacted JSON: the record Live context is made from. */
text: string;
/** The same event, parsed. */
raw: Record<string, any>;
at: number;
/** The agent's turn state after this event. */
state: TurnState;
/** The event came from a subagent. */
child: boolean;
}
/**
* One row of the table that decides what of an agent's events reaches Live: remove
* an event or keys (never sent), or truncate them to a number of characters, cut
* from the middle (1200 keeps the first and last 600; 0 keeps only an omission
* marker). The core applies the rows in order; anything no row names is sent as it is.
*/
export type ContextRule = {
/** The event, by the name the observer gives it (`Observation.name`); every event when absent. */
event?: string;
/** Only events whose fields, by dotted path, have these values, such as `{ tool_name: 'Edit' }` or `{ 'message.role': 'user' }`. */
where?: Record<string, string>;
/** Dotted paths into the event, stepping through every item of a list on the way; the whole event when absent. */
key?: string | readonly string[];
} & ({ remove: true } | { truncate: number });
/** A spoken request on its way to the agent. */
export interface VoiceRequest {
id: string;
content: string;
delegationId?: string | null;
voiceSessionId?: string;
queuedAt?: number;
}
/** How far a delivery got. */
export type DeliveryResult = { state: 'sent' } | { state: 'uncertain'; error: Error };
/** A prompt the agent received that carries a voice request. */
export interface ReceivedRequest {
id: string;
/** The request text inside the agent's envelope, when it can be separated. */
content?: string;
}
/** What the agent's own command line says about the session it opens. */
export interface AgentArguments {
/** Not a session (help, version, a subcommand): run the command without the companion. */
direct: boolean;
/** The session's ID, when the command names it. */
sessionId?: string;
/** The command continues an earlier conversation. */
resume: boolean;
/** Nothing in the command selects a session: the launcher assigns a new session ID. */
assignSession: boolean;
}
/** Everything the core knows about one kind of agent before a session exists. */
export interface AgentDefinition {
profile: AgentProfile;
/** What of its events reaches Live: see ContextRule. */
context: readonly ContextRule[];
/** Reads the agent's own arguments. Throws, with the reason, when one would stop the companion from working. */
readArgs(args: readonly string[]): AgentArguments;
/** Ways to observe assistant text that `--observe` can choose, default first; none if there is no choice. */
observationModes: readonly string[];
/** Finds the voice request inside a prompt the agent received, if there is one. */
receivedRequest(prompt: string): ReceivedRequest | undefined;
/** The tool whose call an event reports finished (by event name and the event's context data), so a burst of tool work can be summed up by tool. */
toolCall?(event: string, data: any): string | undefined;
/**
* What the agent's command hook answers for an event that delivers a voice request this bridge
* sent (`delivered` checks a request ID): how this agent is told the request is the operator's
* own speech, as `note`. Claude labels channel messages as external input and often declines
* them mid-turn without it. Undefined: the hook answers nothing.
*/
vouch?(event: Record<string, any>, delivered: (id: string) => boolean, note: string): unknown;
/**
* A one-off, non-interactive run of this agent with no tools and no saved session, for a short
* piece of writing (a learned instruction): the command, its arguments, and how the prompt goes
* in and the reply comes out. `outputFile` is a path the agent may write its reply to.
*/
ask?(prompt: string, outputFile: string): { command: string; args: string[]; stdin?: string; output?: 'stdout' | 'file' };
/** The exact message deliver() sends for a request, shown in the request inspector. */
wire(request: VoiceRequest): unknown;
create(session: AgentSession): AgentAdapter;
/** Checks the agent's local prerequisites for `npm run doctor`, without API spending. */
doctor(): { report: Record<string, unknown>; ok: boolean };
/** For the launcher's help: example command lines after `fdc`, and a note on running this agent. */
usage: { examples: string[]; notes: string };
}
/** What the bridge gives an adapter for one agent session. */
export interface AgentSession {
root: string;
runDir: string;
cwd: string;
/** The session's ID; when unknown, the adapter learns it from the agent's first event. */
sessionId?: string;
/** How assistant text is observed, for agents that offer a choice. */
observation: string;
/** The agent's own command-line arguments, passed through unchanged. */
agentArgs: string[];
clean: (text: string) => string;
log: (event: Record<string, unknown>) => void;
}
/** The agent's own command, ready to run, and the files written for it. */
export interface AgentLaunch {
command: string;
args: string[];
env: Record<string, string>;
files: Record<string, string>;
}
/** A local WebSocket route an adapter serves on the bridge, authenticated with the agent token. */
export interface AgentSocket {
/** Whether another connection can be accepted now. */
available(): boolean;
attach(ws: WebSocket): void;
}
/**
* One agent session, as the core uses it. Emits:
* - `session` (id) when an unknown session ID is learned from the agent;
* - `connection` (ready: boolean) when requests can, or can no longer, be delivered;
* - `delivery` ({ id, state }) when a request reported uncertain is confirmed later;
* - `update` after any change the status display should reflect;
* - `fault` (error).
*/
export interface AgentAdapter extends EventEmitter {
readonly profile: AgentProfile;
/** Prepares what the agent needs (configuration, helper processes) and returns its command. Throws, with the reason, when the agent cannot run with the companion. */
launch(connection: { baseUrl: string; token: string }): AgentLaunch | Promise<AgentLaunch>;
/** The observer: every agent event as a shared Observation, plus conversation and state. */
readonly observations: AgentObserver;
/** Sends one request and resolves with how far it got. */
deliver(request: VoiceRequest): Promise<DeliveryResult>;
/** Observations so far, oldest first, including any restored from a resumed session. */
history(): readonly Observation[];
readonly sockets: Readonly<Record<string, AgentSocket>>;
/** Stops delivering and observing, and anything launch() started. */
close(): void | Promise<void>;
}