@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.
@@ -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 };