loopengine 0.0.21 → 0.1.3

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.
Files changed (38) hide show
  1. package/PROTOCOL.md +3 -3
  2. package/README.md +45 -10
  3. package/dist/core/agent-config.d.ts +291 -8
  4. package/dist/core/budget.d.ts +42 -0
  5. package/dist/core/budget.js +39 -0
  6. package/dist/core/compaction.d.ts +43 -0
  7. package/dist/core/compaction.js +70 -0
  8. package/dist/core/http-notifier.d.ts +10 -0
  9. package/dist/core/http-notifier.js +116 -0
  10. package/dist/core/http-notify-triggers/database.d.ts +19 -0
  11. package/dist/core/http-notify-triggers/database.js +53 -0
  12. package/dist/core/http-notify-triggers/email.d.ts +44 -0
  13. package/dist/core/http-notify-triggers/email.js +126 -0
  14. package/dist/core/http-notify-triggers/lark.d.ts +28 -0
  15. package/dist/core/http-notify-triggers/lark.js +168 -0
  16. package/dist/core/http-notify-triggers/redis.d.ts +33 -0
  17. package/dist/core/http-notify-triggers/redis.js +54 -0
  18. package/dist/core/http-notify-triggers/slack.d.ts +21 -0
  19. package/dist/core/http-notify-triggers/slack.js +91 -0
  20. package/dist/core/http-notify-triggers/webhook.d.ts +27 -0
  21. package/dist/core/http-notify-triggers/webhook.js +94 -0
  22. package/dist/core/loop-events.d.ts +30 -6
  23. package/dist/core/model-calls/openai-model-call.js +32 -16
  24. package/dist/core/recovery.d.ts +49 -0
  25. package/dist/core/recovery.js +124 -0
  26. package/dist/core/run-agent.d.ts +141 -13
  27. package/dist/core/run-agent.js +316 -57
  28. package/dist/core/session-store.d.ts +1 -0
  29. package/dist/core/session-store.js +6 -4
  30. package/dist/core/system-skills/system-skills/composio-large-outputs/SKILL.md +25 -0
  31. package/dist/core/system-tools/ask_user.d.ts +97 -31
  32. package/dist/core/system-tools/ask_user.js +157 -58
  33. package/dist/core/system-tools/index.d.ts +1 -1
  34. package/dist/core/system-tools/index.js +1 -1
  35. package/dist/package.json +6 -0
  36. package/package.json +14 -4
  37. package/protocol/loop-event.schema.json +2 -2
  38. /package/{core → dist/core}/system-skills/composio-large-outputs/SKILL.md +0 -0
@@ -1,5 +1,5 @@
1
1
  import { RuleSet, type Approver } from 'actauth';
2
- import type { AgentConfig, ToolDefinition, ToolSchema } from '#core/agent-config.js';
2
+ import type { AgentConfig, ApproverChannel, QuestionHandler, ToolDefinition, ToolSchema } from '#core/agent-config.js';
3
3
  import { type PendingQuestion } from './system-tools/index.js';
4
4
  import type { LoopEvent } from './loop-events.js';
5
5
  export { systemTools } from './system-tools/index.js';
@@ -8,7 +8,18 @@ export declare const systemSkillsDir: string;
8
8
  export declare function dedupeToolsByName(list: ToolDefinition[]): ToolDefinition[];
9
9
  export interface ModelContentBlock {
10
10
  type: string;
11
- /** text blocks. */
11
+ /** text blocks, and thinking blocks (a model's own chain-of-thought,
12
+ * e.g. DeepSeek's `reasoning_content` sibling field, or — if a
13
+ * model-calls/* adapter ever opts into it — Anthropic's own `thinking`
14
+ * content block). Reuses this same field rather than adding a
15
+ * thinking-only one, since both are just "a string of the model's own
16
+ * words." Carried opaquely: loopengine never reads or reasons about
17
+ * this text itself, only round-trips it back verbatim on the next
18
+ * request that includes this message — some reasoning-mode providers
19
+ * (DeepSeek's thinking mode, confirmed live; Anthropic's extended
20
+ * thinking, documented the same way) reject the request outright if a
21
+ * prior turn's reasoning gets silently dropped on the way back in,
22
+ * once the conversation has tool_calls in it. */
12
23
  text?: string;
13
24
  /** tool_use blocks: this call's own id (referenced by a later tool_result). */
14
25
  id?: string;
@@ -73,15 +84,32 @@ export interface RunAgentOptions {
73
84
  * pending question can't be session-scoped — still fine, it's still
74
85
  * agent-scoped either way. */
75
86
  sessionId?: string;
76
- /** Default Approver for this call's 'ask' decisions, when
77
- * AgentConfig.approver isn't set — the seam an adapter uses to pick
78
- * whichever approver actually fits its own channel (ConsoleApprover for
79
- * a real terminal, a web-based one for an HTTP-served conversation,
80
- * ...) without every agent needing to hardcode one itself. An explicit
81
- * AgentConfig.approver still wins outright over this when the agent
82
- * author set one on purpose — same as it always has (see Gate
83
- * construction below) — this only fills in when that's absent. Default,
84
- * if neither is given: actauth's own ConsoleApprover, same as always. */
87
+ /** Which channel this call is on — checked below against `'http'`,
88
+ * the only value `AgentConfig.httpNotifier` ever matches (see that
89
+ * field's own doc comment). Omitted (a standalone script, a bespoke
90
+ * dispatcher with no fixed channel identity) means `httpNotifier` is
91
+ * never consulted at all — `approver` below is the only thing that
92
+ * applies. */
93
+ channel?: ApproverChannel;
94
+ /** This call's own default Approver for 'ask' decisions, when
95
+ * `AgentConfig.httpNotifier` doesn't supply one (only possible on the
96
+ * `http` channel, and only when it lists `'approval'`) — the seam an
97
+ * adapter uses to pick whichever approver actually fits its own channel
98
+ * (ConsoleApprover for a real terminal, a fresh WebchatApprover per
99
+ * streamed turn, a WebhookApprover for a background dispatcher,
100
+ * ...) without every agent needing to hardcode one itself. `http`'s own
101
+ * `httpNotifier`, if it covers `'approval'`, still wins outright over
102
+ * this — the agent author's own explicit choice *for that channel*
103
+ * beats whatever the adapter would otherwise default to (see Gate
104
+ * construction below) — `cli`/`http_stream` have no such override at
105
+ * all, so this is the only thing that ever applies there. Default, if
106
+ * neither is given: actauth's own ConsoleApprover, same as always.
107
+ *
108
+ * Can be a DurableApprover (WebhookApprover, ...) instead of a live
109
+ * one — see HUMAN_IN_THE_LOOP.md. A background/cron dispatcher calling
110
+ * runAgent() directly is exactly this same kind of caller, and should
111
+ * pass its own durable default here the same way adapters/http.ts's
112
+ * stream route passes a fresh WebchatApprover. */
85
113
  approver?: Approver;
86
114
  /** Fired the instant the system ask_user tool registers a new pending
87
115
  * question — this is the seam that decides between a real, answerable
@@ -100,6 +128,30 @@ export interface RunAgentOptions {
100
128
  * that genuinely has an answering mechanism (adapters/http.ts, for both
101
129
  * its routes) should pass this. */
102
130
  onQuestionPending?: (question: PendingQuestion) => void;
131
+ /** This call's own default QuestionHandler — question-side sibling of
132
+ * `approver` above, same precedence (`AgentConfig.httpNotifier`, when it
133
+ * covers `'question'` on the `http` channel, still wins outright over
134
+ * this) and the same `LiveQuestionHandler | DurableQuestionHandler`
135
+ * union `approver` itself is, duck-typed the same way (see
136
+ * `isDurableQuestionHandler`). Default, if neither this nor
137
+ * `httpNotifier` supplies one: `new CliQuestionHandler()` — same role
138
+ * `new ConsoleApprover()` plays for `approver`.
139
+ *
140
+ * One real, current asymmetry versus `approver`: only the *durable*
141
+ * half of this union is actually wired into the loop below — a
142
+ * `system_ask_user` call only ever consults this field to decide
143
+ * live-vs-durable; the live case still always falls through to
144
+ * `onQuestionPending`/the tool's own independent live resolution
145
+ * (unchanged from before this field existed), never actually calling
146
+ * a live `QuestionHandler` resolved here. See HUMAN_IN_THE_LOOP.md's
147
+ * "Durable questions" section for why: fully wiring the live half too
148
+ * would mean moving system_ask_user's answer-collection inline into
149
+ * this loop (changing today's batching semantics — right now a
150
+ * pending question is deferred past the whole tool_use batch scan,
151
+ * unlike a live approval) and a breaking change to createAskUserTool's
152
+ * own signature — deliberately deferred rather than done half-carefully
153
+ * here. */
154
+ questionHandler?: QuestionHandler;
103
155
  }
104
156
  export interface RunAgentResult {
105
157
  /** Final assistant text for this turn. */
@@ -132,8 +184,62 @@ export interface RunAgentResult {
132
184
  * model requested in the same turn, even one already approved, never
133
185
  * runs either (see the loop's own body); it gets a "skipped", not a
134
186
  * "denied", tool_result, since it was never evaluated against its own
135
- * rule. */
136
- stopReason?: 'max_turns' | 'denied';
187
+ * rule.
188
+ *
189
+ * 'pending_approval' / 'pending_question': at least one requested tool
190
+ * call is durably pending a DurableApprover decision, or at least one
191
+ * `system_ask_user` call is durably pending a DurableQuestionHandler
192
+ * answer (see HUMAN_IN_THE_LOOP.md) — same "the whole batch stops here"
193
+ * shape as 'denied', except this isn't a rejection: any already-run
194
+ * sibling call in the same batch still ran (see the loop's own
195
+ * bucket-then-execute), captured in `pending.resultsSoFar`, and the
196
+ * turn resumes later via resumeAgent() once every pendingId in
197
+ * `pending.outstanding` is resolved — not by sending another message
198
+ * the way 'denied' recovers. A batch can mix both kinds (a gated tool
199
+ * call and an `system_ask_user` call in the same model response) — both
200
+ * land in the same `pending.outstanding`, since the one dangling
201
+ * assistant tool_use message can only ever get one completing
202
+ * tool_result message; `stopReason` reports 'pending_approval' for that
203
+ * mixed case too (only a batch with *no* approval items at all reports
204
+ * 'pending_question'). `newMessages` ends at the dangling assistant
205
+ * tool_use message, same crash-recovery shape session-store.ts's own
206
+ * hasUnresolvedToolCall already detects — entered on purpose here,
207
+ * not by accident. runAgent() does no I/O (see this module's own
208
+ * header comment): turning `pending` into a durable TurnCheckpoint
209
+ * (core/durable-approvals.ts) is the caller's job. */
210
+ stopReason?: 'max_turns' | 'denied' | 'pending_approval' | 'pending_question';
211
+ /** Set only when stopReason is 'pending_approval' or 'pending_question'. */
212
+ pending?: {
213
+ /** tool_result blocks already computed this batch — safely
214
+ * auto-allowed calls that ran immediately despite a gated/asked
215
+ * sibling still being outstanding. Not yet pushed into `newMessages`/
216
+ * `history`: incomplete until every outstanding item below is
217
+ * resolved too, and a real model API expects one complete
218
+ * tool_result message per assistant tool_use message, not a partial
219
+ * one now and a follow-up later. */
220
+ resultsSoFar: ModelContentBlock[];
221
+ /** One entry per tool call still awaiting a durable decision, keyed
222
+ * by the pendingId its DurableApprover/DurableQuestionHandler handed
223
+ * out — what an incoming resolution actually names. `kind`
224
+ * distinguishes which of the two this is — for 'question', `args` is
225
+ * `{ question, options }` and `reason` is the question text itself
226
+ * (there's no separate ActAuth "reason" for a question the way there
227
+ * is for a gated tool call's decision). */
228
+ outstanding: PendingItem[];
229
+ };
230
+ }
231
+ /** One requested call still awaiting a durable decision — either a gated
232
+ * tool call (`kind: 'approval'`) or a `system_ask_user` call (`kind:
233
+ * 'question'`), unified into one array so a batch mixing both shares
234
+ * exactly one checkpoint (see RunAgentResult.stopReason's own doc
235
+ * comment for why that has to be true). */
236
+ export interface PendingItem {
237
+ kind: 'approval' | 'question';
238
+ toolUseId: string;
239
+ tool: string;
240
+ args: Record<string, unknown>;
241
+ pendingId: string;
242
+ reason: string;
137
243
  }
138
244
  /** Resolves AgentConfig.rules the same way skillsDirs resolves its own
139
245
  * default: an inline array is used as-is (full 3-segment scopePattern,
@@ -180,3 +286,25 @@ export declare function loadDefaultTools(config: AgentConfig): Promise<ToolDefin
180
286
  * elsewhere could. */
181
287
  export declare function loadSubagentAsTools(config: AgentConfig): Promise<ToolDefinition[]>;
182
288
  export declare function runAgent(config: AgentConfig, modelCall: ModelCall, userMessage: string, history?: Message[], options?: RunAgentOptions): Promise<RunAgentResult>;
289
+ /** Continues a turn durably paused on RunAgentResult.stopReason ===
290
+ * 'pending_approval' — see HUMAN_IN_THE_LOOP.md for the full design.
291
+ * `history` is durable session history including the dangling assistant
292
+ * tool_use message the pause left behind (exactly what a caller's
293
+ * SessionStore.getHistory/withSession already returns — this needs no
294
+ * special resumed-session handling, since that message is real, already
295
+ * on disk). `resolution` is the *complete* set of tool_result blocks
296
+ * answering that message — every entry from a resolved TurnCheckpoint's
297
+ * own resultsSoFar, once its outstanding is empty; assembling that is the
298
+ * caller's job (core/durable-approvals.ts's own CheckpointStore only
299
+ * stores it, it doesn't execute tools or know when a batch is complete —
300
+ * see that module's own doc comment), not something this function
301
+ * derives on its own.
302
+ *
303
+ * Pushes `resolution` as the completing message and picks the loop back
304
+ * up from there — a fresh model call, now with the full picture, exactly
305
+ * like a synchronous 'ask' would have continued if it hadn't needed to
306
+ * pause at all. Can itself return with stopReason 'pending_approval'
307
+ * again (the model's next round of tool calls hits another durably-gated
308
+ * one), 'denied', 'max_turns', or a genuine finish — runLoop handles all
309
+ * of those uniformly regardless of how this turn started. */
310
+ export declare function resumeAgent(config: AgentConfig, modelCall: ModelCall, history: Message[], resolution: ModelContentBlock[], options?: RunAgentOptions): Promise<RunAgentResult>;