@openape/ape-agent 2.9.2 → 2.11.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/dist/bridge.mjs +1255 -325
- package/dist/index.d.ts +356 -0
- package/dist/index.mjs +4923 -0
- package/dist/service-bridge-main.mjs +4440 -0
- package/package.json +16 -4
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { DecisionResult } from '@openape/prompt-injection-detector';
|
|
2
|
+
import { RuntimeConfig } from '@openape/apes';
|
|
3
|
+
|
|
4
|
+
declare const REASONING_EFFORTS: readonly ["minimal", "low", "medium", "high"];
|
|
5
|
+
type ReasoningEffort = typeof REASONING_EFFORTS[number];
|
|
6
|
+
/**
|
|
7
|
+
* Telegram chat-adapter config. Present only when the owner has bound a bot
|
|
8
|
+
* token to this agent (delivered as a sealed secret → bridge env). Its mere
|
|
9
|
+
* presence activates the Telegram channel — no separate toggle.
|
|
10
|
+
*/
|
|
11
|
+
interface TelegramConfig {
|
|
12
|
+
botToken: string;
|
|
13
|
+
/**
|
|
14
|
+
* The one Telegram user id allowed to drive the bot. Optional: when unset,
|
|
15
|
+
* the channel pins the first user who messages it as the owner (trust on
|
|
16
|
+
* first use) and persists that — so the owner only has to supply the token.
|
|
17
|
+
* Set it explicitly to hard-lock without the first-contact step.
|
|
18
|
+
*/
|
|
19
|
+
ownerUserId?: number;
|
|
20
|
+
}
|
|
21
|
+
interface BridgeConfig {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
apesBin: string;
|
|
24
|
+
model: string;
|
|
25
|
+
/**
|
|
26
|
+
* Reasoning/thinking depth for gpt-5.x. Lets the PM tier compute by task
|
|
27
|
+
* difficulty without changing the model. Omitted = proxy/model default.
|
|
28
|
+
*/
|
|
29
|
+
reasoningEffort?: ReasoningEffort;
|
|
30
|
+
systemPrompt: string;
|
|
31
|
+
tools: string[];
|
|
32
|
+
maxSteps: number;
|
|
33
|
+
roomFilter?: string;
|
|
34
|
+
/** Optional Telegram adapter — set when TELEGRAM_BOT_TOKEN is in the env. */
|
|
35
|
+
telegram?: TelegramConfig;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the bridge config from an env map. Defaults to `process.env` so the
|
|
39
|
+
* bin entrypoint is unchanged; the env is injectable so the nest's in-process
|
|
40
|
+
* SessionHost can resolve one config per agent (per-agent model merged into the
|
|
41
|
+
* map) without depending on or mutating the daemon's global `process.env`.
|
|
42
|
+
*/
|
|
43
|
+
declare function readConfig(env?: NodeJS.ProcessEnv): BridgeConfig;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A decoded inbound troop chat frame worth acting on: the chat it belongs to
|
|
47
|
+
* plus the raw payload (`{id, chatId, role, body, ...}`). Produced by
|
|
48
|
+
* {@link AgentSession.parseChatFrame} after the protocol envelope is stripped.
|
|
49
|
+
*/
|
|
50
|
+
interface TroopChatFrame {
|
|
51
|
+
chatId: string;
|
|
52
|
+
payload: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A troop chat message in the chat.openape.ai-style shape the agent loop
|
|
56
|
+
* consumes — the input `runLoop` runs on. Produced by {@link AgentSession.toMessage}
|
|
57
|
+
* from a {@link TroopChatFrame}.
|
|
58
|
+
*/
|
|
59
|
+
interface TroopMessage {
|
|
60
|
+
id: string;
|
|
61
|
+
roomId: string;
|
|
62
|
+
threadId: string;
|
|
63
|
+
senderEmail: string;
|
|
64
|
+
senderAct: 'human' | 'agent';
|
|
65
|
+
body: string;
|
|
66
|
+
replyTo: string | null;
|
|
67
|
+
createdAt: number;
|
|
68
|
+
editedAt: number | null;
|
|
69
|
+
}
|
|
70
|
+
declare class AgentSession {
|
|
71
|
+
readonly email: string;
|
|
72
|
+
readonly ownerEmail: string;
|
|
73
|
+
readonly config: BridgeConfig;
|
|
74
|
+
/**
|
|
75
|
+
* Lazily-created prompt-injection detector, shared across this session's
|
|
76
|
+
* messages. Matches the per-agent bridge, which holds one
|
|
77
|
+
* `createHeuristicDetector()` for its lifetime.
|
|
78
|
+
*/
|
|
79
|
+
private injectionDetector;
|
|
80
|
+
constructor(email: string, ownerEmail: string, config: BridgeConfig);
|
|
81
|
+
describe(): string;
|
|
82
|
+
/**
|
|
83
|
+
* Build this agent's troop chat WebSocket URL from its resolved endpoint and
|
|
84
|
+
* a bearer token. Ports the exact derivation the per-agent bridge uses in
|
|
85
|
+
* `pumpOnce` (http→ws, token carried as a query param, a leading `Bearer `
|
|
86
|
+
* prefix stripped, the value URL-encoded) so the nest's in-process WS-open
|
|
87
|
+
* increment connects to the same socket the bridge process opens today — with
|
|
88
|
+
* no second copy of the URL rule once the nest drives the connection.
|
|
89
|
+
*/
|
|
90
|
+
chatSocketUrl(bearer: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Decode one raw troop chat-socket frame into a {@link TroopChatFrame}, or
|
|
93
|
+
* `null` for frames the agent ignores. Ports the exact decode + filter the
|
|
94
|
+
* per-agent bridge applies in `pumpOnce`: tolerate string or `Buffer` data,
|
|
95
|
+
* skip anything that is not valid JSON, and keep only `{type:'message'}`
|
|
96
|
+
* frames that carry a payload. This is the canonical home for the framing
|
|
97
|
+
* rule once the nest drives the connection — the WS-message increment routes
|
|
98
|
+
* accepted frames into the agent loop with no second copy of the rule.
|
|
99
|
+
*/
|
|
100
|
+
parseChatFrame(data: unknown): TroopChatFrame | null;
|
|
101
|
+
/**
|
|
102
|
+
* Translate an accepted {@link TroopChatFrame} into the {@link TroopMessage}
|
|
103
|
+
* the agent loop runs on. Ports the bridge's `translateTroopPayload`: troop's
|
|
104
|
+
* payload carries `role` (human|agent) but no sender email, so the email is
|
|
105
|
+
* synthesized from role (agent → this session's own email, human → the owner)
|
|
106
|
+
* — the bridge skips its own echoes via `senderEmail === selfEmail`, so this
|
|
107
|
+
* mapping must match. `threadId` is the synthetic `'main'` because troop has
|
|
108
|
+
* no threads. This is the canonical home for the payload→message rule once the
|
|
109
|
+
* nest drives the connection: the runLoop-dispatch increment feeds this
|
|
110
|
+
* message straight into the loop with no second copy of the translation.
|
|
111
|
+
*/
|
|
112
|
+
toMessage(frame: TroopChatFrame): TroopMessage;
|
|
113
|
+
/**
|
|
114
|
+
* Whether a translated {@link TroopMessage} is this agent's own echo. troop
|
|
115
|
+
* fans every chat message back to the socket that sent it, so the agent sees
|
|
116
|
+
* its own replies; feeding those into the loop would be an infinite feedback
|
|
117
|
+
* cycle. Ports the bridge's `handleInbound` guard (`senderEmail === selfEmail`)
|
|
118
|
+
* — the canonical home for the self-echo rule once the nest drives the
|
|
119
|
+
* connection: the runLoop-dispatch increment skips own echoes before it runs
|
|
120
|
+
* the loop, with no second copy of the comparison.
|
|
121
|
+
*/
|
|
122
|
+
isOwnEcho(message: TroopMessage): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Whether a translated, non-echo {@link TroopMessage} should reach the agent
|
|
125
|
+
* loop. Ports the bridge's remaining pre-loop guards in `handleInbound`: an
|
|
126
|
+
* empty or whitespace-only body carries nothing to act on, and a configured
|
|
127
|
+
* `roomFilter` scopes the agent to a single chat. (The bridge's `threadId`
|
|
128
|
+
* guard is moot here — {@link toMessage} always synthesizes `'main'`.) The
|
|
129
|
+
* own-echo guard stays {@link isOwnEcho}, applied first by the caller. This is
|
|
130
|
+
* the canonical home for the dispatch-filter rule once the nest drives the
|
|
131
|
+
* connection: the runLoop-dispatch increment runs the loop only for messages
|
|
132
|
+
* this accepts, with no second copy of the guards.
|
|
133
|
+
*/
|
|
134
|
+
shouldDispatch(message: TroopMessage): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Screen an accepted, non-echo {@link TroopMessage} for prompt injection
|
|
137
|
+
* before it reaches the agent loop. Ports the bridge's `handleInbound`
|
|
138
|
+
* choke-point: the bridge runs every inbound message through a heuristic
|
|
139
|
+
* detector and refuses to forward it when the score crosses the threshold,
|
|
140
|
+
* because once the text is in the loop's history a refusal is harder and
|
|
141
|
+
* inconsistent. The owner gets a higher bar (legitimate "run shell, do X"
|
|
142
|
+
* instructions aren't refused) — handled by `decide` keying the threshold off
|
|
143
|
+
* `sender.isOwner`. This is the canonical home for the screening rule once the
|
|
144
|
+
* nest drives the connection: the runLoop-dispatch increment refuses blocked
|
|
145
|
+
* messages with no second copy of the detector setup or the sender mapping.
|
|
146
|
+
*/
|
|
147
|
+
screenInjection(message: TroopMessage): Promise<DecisionResult>;
|
|
148
|
+
/**
|
|
149
|
+
* The short, neutral refusal the agent posts back when {@link screenInjection}
|
|
150
|
+
* blocks a message. Ports the bridge's `refusalText`: the matched reason is
|
|
151
|
+
* appended so the owner sees in their chat history + audit log why a specific
|
|
152
|
+
* message was blocked, but the phrasing deliberately avoids language an
|
|
153
|
+
* attacker could copy back ("ignore previous instructions and …") to
|
|
154
|
+
* re-trigger the detector. This is the canonical home for the refusal-message
|
|
155
|
+
* rule once the nest drives the connection: the runLoop-dispatch increment
|
|
156
|
+
* posts this text on a block with no second copy of the wording.
|
|
157
|
+
*/
|
|
158
|
+
refusalText(reason: string | undefined): string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface AgentIdentity {
|
|
162
|
+
email: string;
|
|
163
|
+
ownerEmail: string;
|
|
164
|
+
idp: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Read the agent's identity from auth.json. Throws if the file is
|
|
168
|
+
* missing or has no `email` — both indicate a botched spawn.
|
|
169
|
+
*
|
|
170
|
+
* `owner_email` is written by `apes agents spawn`. If it's missing we
|
|
171
|
+
* fall back to `OPENAPE_OWNER_EMAIL` from the container environment
|
|
172
|
+
* (compose `environment:` block) so an old auth.json that pre-dates
|
|
173
|
+
* Phase A doesn't strand the bridge in a crash loop. If both are
|
|
174
|
+
* missing we throw — the bridge requires it for the contact handshake.
|
|
175
|
+
*
|
|
176
|
+
* `home` defaults to the running process's home, which is the bin path's
|
|
177
|
+
* behaviour (each per-agent bridge ran as its own OS user). The nest's
|
|
178
|
+
* in-process SessionHost passes the registry entry's `home` so one daemon
|
|
179
|
+
* can read each hosted agent's identity from that agent's own home.
|
|
180
|
+
*/
|
|
181
|
+
declare function readAgentIdentity(home?: string): AgentIdentity;
|
|
182
|
+
|
|
183
|
+
interface PostedMessage {
|
|
184
|
+
id: string;
|
|
185
|
+
roomId: string;
|
|
186
|
+
threadId: string;
|
|
187
|
+
body: string;
|
|
188
|
+
createdAt: number;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* One row from chat history. Used by the bridge to backfill
|
|
192
|
+
* ThreadSession message history after a process restart.
|
|
193
|
+
*/
|
|
194
|
+
interface HistoryMessage {
|
|
195
|
+
id: string;
|
|
196
|
+
roomId: string;
|
|
197
|
+
threadId: string;
|
|
198
|
+
senderEmail: string;
|
|
199
|
+
senderAct: 'human' | 'agent';
|
|
200
|
+
body: string;
|
|
201
|
+
replyTo: string | null;
|
|
202
|
+
createdAt: number;
|
|
203
|
+
}
|
|
204
|
+
interface ContactView {
|
|
205
|
+
peerEmail: string;
|
|
206
|
+
myStatus: 'accepted' | 'pending' | 'blocked';
|
|
207
|
+
theirStatus: 'accepted' | 'pending' | 'blocked';
|
|
208
|
+
connected: boolean;
|
|
209
|
+
roomId: string | null;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Structural interface both the cron-runner and thread-session use
|
|
213
|
+
* so their call sites stay backend-agnostic.
|
|
214
|
+
*/
|
|
215
|
+
interface ChatBackend {
|
|
216
|
+
postMessage: (roomId: string, body: string, opts?: {
|
|
217
|
+
replyTo?: string;
|
|
218
|
+
threadId?: string;
|
|
219
|
+
streaming?: boolean;
|
|
220
|
+
}) => Promise<PostedMessage>;
|
|
221
|
+
listMessages: (roomId: string, threadId: string, limit?: number) => Promise<HistoryMessage[]>;
|
|
222
|
+
patchMessage: (messageId: string, opts?: {
|
|
223
|
+
body?: string;
|
|
224
|
+
streaming?: boolean;
|
|
225
|
+
streamingStatus?: string | null;
|
|
226
|
+
}) => Promise<void>;
|
|
227
|
+
listContacts: () => Promise<ContactView[]>;
|
|
228
|
+
requestContact: (peerEmail: string) => Promise<ContactView>;
|
|
229
|
+
acceptContact: (peerEmail: string) => Promise<ContactView>;
|
|
230
|
+
createThread: (roomId: string, name: string) => Promise<{
|
|
231
|
+
id: string;
|
|
232
|
+
name: string;
|
|
233
|
+
}>;
|
|
234
|
+
}
|
|
235
|
+
declare class TroopChatApi {
|
|
236
|
+
private endpoint;
|
|
237
|
+
private bearer;
|
|
238
|
+
private bootstrap;
|
|
239
|
+
constructor(endpoint: string, bearer: () => Promise<string>);
|
|
240
|
+
/** Resolve + cache the agent's chat row (lazy fetch on first use). */
|
|
241
|
+
private getBootstrap;
|
|
242
|
+
/** chat.id + (lazy-fetched) ownerEmail for the bridge's frame-translation path. */
|
|
243
|
+
getChatContext(): Promise<{
|
|
244
|
+
chatId: string;
|
|
245
|
+
ownerEmail: string;
|
|
246
|
+
agentEmail: string;
|
|
247
|
+
}>;
|
|
248
|
+
postMessage(roomId: string, body: string, opts?: {
|
|
249
|
+
replyTo?: string;
|
|
250
|
+
threadId?: string;
|
|
251
|
+
streaming?: boolean;
|
|
252
|
+
}): Promise<PostedMessage>;
|
|
253
|
+
listMessages(roomId: string, threadId: string, limit?: number): Promise<HistoryMessage[]>;
|
|
254
|
+
patchMessage(messageId: string, opts?: {
|
|
255
|
+
body?: string;
|
|
256
|
+
streaming?: boolean;
|
|
257
|
+
streamingStatus?: string | null;
|
|
258
|
+
}): Promise<void>;
|
|
259
|
+
/**
|
|
260
|
+
* Troop's chat doesn't have contacts — synthesize a single
|
|
261
|
+
* always-connected entry pointing at the owner so the bridge's
|
|
262
|
+
* initial-contact + allowlist flows are no-ops.
|
|
263
|
+
*/
|
|
264
|
+
listContacts(): Promise<ContactView[]>;
|
|
265
|
+
requestContact(peerEmail: string): Promise<ContactView>;
|
|
266
|
+
acceptContact(peerEmail: string): Promise<ContactView>;
|
|
267
|
+
/**
|
|
268
|
+
* Troop has no threads — return a synthetic one. The bridge's
|
|
269
|
+
* cron-runner falls back to the main thread on createThread
|
|
270
|
+
* failure already, so a stable "main" stand-in is the right shape.
|
|
271
|
+
*/
|
|
272
|
+
createThread(roomId: string, name: string): Promise<{
|
|
273
|
+
id: string;
|
|
274
|
+
name: string;
|
|
275
|
+
}>;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface ThreadSessionDeps {
|
|
279
|
+
roomId: string;
|
|
280
|
+
threadId: string;
|
|
281
|
+
chat: ChatBackend;
|
|
282
|
+
/** LiteLLM proxy + model — the bridge resolves these from its env. */
|
|
283
|
+
runtimeConfig: RuntimeConfig;
|
|
284
|
+
/**
|
|
285
|
+
* Resolve the runtimeConfig fresh at the start of every turn. The gateway
|
|
286
|
+
* bearer is a short-lived (1h) DDISA-exchanged token; a long-lived thread
|
|
287
|
+
* that froze it at construction would present an expired token and get a
|
|
288
|
+
* 401. When provided, this is awaited per turn so the token stays fresh;
|
|
289
|
+
* falls back to the static `runtimeConfig` when absent (tests).
|
|
290
|
+
*/
|
|
291
|
+
refreshRuntimeConfig?: () => Promise<RuntimeConfig>;
|
|
292
|
+
/**
|
|
293
|
+
* Resolve systemPrompt + tools at the start of every turn rather
|
|
294
|
+
* than freezing them at construction. Lets owner edits in the
|
|
295
|
+
* troop UI (which sync to `~/.openape/agent/agent.json` via
|
|
296
|
+
* `apes agents sync`) take effect on the next message in an
|
|
297
|
+
* existing thread — not just on freshly-opened threads.
|
|
298
|
+
* `tools` is the string list that `taskTools()` resolves to the
|
|
299
|
+
* concrete `ToolDefinition[]`.
|
|
300
|
+
*/
|
|
301
|
+
resolveConfig: () => {
|
|
302
|
+
systemPrompt: string;
|
|
303
|
+
tools: string[];
|
|
304
|
+
};
|
|
305
|
+
/**
|
|
306
|
+
* Agent's own DDISA email — used to classify backfilled messages:
|
|
307
|
+
* `senderEmail === selfEmail` → role='assistant', else → 'user'.
|
|
308
|
+
*/
|
|
309
|
+
selfEmail: string;
|
|
310
|
+
maxSteps: number;
|
|
311
|
+
/** Logger sink — bridge typically forwards to stderr. */
|
|
312
|
+
log: (line: string) => void;
|
|
313
|
+
}
|
|
314
|
+
declare class ThreadSession {
|
|
315
|
+
private deps;
|
|
316
|
+
private active;
|
|
317
|
+
private queue;
|
|
318
|
+
private history;
|
|
319
|
+
/**
|
|
320
|
+
* Whether we've already backfilled history from the chat server.
|
|
321
|
+
* Done lazily on the first turn so a freshly-created ThreadSession
|
|
322
|
+
* (e.g. after a bridge restart) sees the full conversation context,
|
|
323
|
+
* not just the message that woke it up. We skip the message that
|
|
324
|
+
* triggered the turn — runLoop adds it via `userMessage`.
|
|
325
|
+
*/
|
|
326
|
+
private backfilled;
|
|
327
|
+
constructor(deps: ThreadSessionDeps);
|
|
328
|
+
/**
|
|
329
|
+
* No-op placeholder kept for API compatibility with the previous
|
|
330
|
+
* RPC-listener model where dispose() detached the listener.
|
|
331
|
+
*/
|
|
332
|
+
dispose(): void;
|
|
333
|
+
/** Forward an inbound chat message to the runtime. Queues if a turn is in flight. */
|
|
334
|
+
enqueue(body: string, replyToMessageId: string): void;
|
|
335
|
+
private startTurn;
|
|
336
|
+
/**
|
|
337
|
+
* Fetch recent chat history for this thread and seed `this.history`.
|
|
338
|
+
* Idempotent — only runs once per ThreadSession instance. Skips the
|
|
339
|
+
* placeholder we just posted plus the inbound message that triggered
|
|
340
|
+
* this turn (runLoop's `userMessage` handles that one).
|
|
341
|
+
*
|
|
342
|
+
* Failures are non-fatal: we log and continue with empty history.
|
|
343
|
+
* That preserves the pre-backfill behaviour rather than failing the
|
|
344
|
+
* turn over a transient chat-server hiccup.
|
|
345
|
+
*/
|
|
346
|
+
private backfillHistoryOnce;
|
|
347
|
+
/**
|
|
348
|
+
* Stream-end: flush any pending throttled body PATCH, then mark the
|
|
349
|
+
* message as no-longer-streaming. The combined call also triggers
|
|
350
|
+
* the user-facing push (the placeholder POST suppressed it).
|
|
351
|
+
*/
|
|
352
|
+
private endTurn;
|
|
353
|
+
private failTurn;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export { type AgentIdentity, AgentSession, type BridgeConfig, ThreadSession, type ThreadSessionDeps, TroopChatApi, type TroopChatFrame, type TroopMessage, readAgentIdentity, readConfig };
|