@moxxy/sdk 0.44.0 → 0.46.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/agent-account-limit.d.ts +41 -0
- package/dist/agent-account-limit.d.ts.map +1 -0
- package/dist/agent-account-limit.js +64 -0
- package/dist/agent-account-limit.js.map +1 -0
- package/dist/agent-computer-use.d.ts +10 -0
- package/dist/agent-computer-use.d.ts.map +1 -0
- package/dist/agent-computer-use.js +24 -0
- package/dist/agent-computer-use.js.map +1 -0
- package/dist/agent-context-window.d.ts +22 -0
- package/dist/agent-context-window.d.ts.map +1 -0
- package/dist/agent-context-window.js +40 -0
- package/dist/agent-context-window.js.map +1 -0
- package/dist/agent-no-reply.d.ts +11 -0
- package/dist/agent-no-reply.d.ts.map +1 -0
- package/dist/agent-no-reply.js +13 -0
- package/dist/agent-no-reply.js.map +1 -0
- package/dist/agent-step.d.ts +14 -0
- package/dist/agent-step.d.ts.map +1 -0
- package/dist/agent-step.js +26 -0
- package/dist/agent-step.js.map +1 -0
- package/dist/agent-work.d.ts +34 -0
- package/dist/agent-work.d.ts.map +1 -0
- package/dist/agent-work.js +80 -0
- package/dist/agent-work.js.map +1 -0
- package/dist/attachment-payload.d.ts +1 -1
- package/dist/attachment-payload.js +1 -1
- package/dist/auto-approve.d.ts +9 -0
- package/dist/auto-approve.d.ts.map +1 -1
- package/dist/auto-approve.js +18 -0
- package/dist/auto-approve.js.map +1 -1
- package/dist/channel-auth.d.ts +3 -3
- package/dist/channel-auth.d.ts.map +1 -1
- package/dist/channel-auth.js +1 -1
- package/dist/channel.d.ts +1 -1
- package/dist/client-session.d.ts +9 -5
- package/dist/client-session.d.ts.map +1 -1
- package/dist/computer-control.d.ts +11 -11
- package/dist/computer-control.d.ts.map +1 -1
- package/dist/computer-control.js +1 -1
- package/dist/computer-control.js.map +1 -1
- package/dist/conversation-history.d.ts +50 -0
- package/dist/conversation-history.d.ts.map +1 -0
- package/dist/conversation-history.js +227 -0
- package/dist/conversation-history.js.map +1 -0
- package/dist/cron.d.ts +58 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +320 -0
- package/dist/cron.js.map +1 -0
- package/dist/define.d.ts +2 -1
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +1 -0
- package/dist/define.js.map +1 -1
- package/dist/event-store.d.ts +1 -1
- package/dist/event-store.d.ts.map +1 -1
- package/dist/event-store.js +1 -0
- package/dist/event-store.js.map +1 -1
- package/dist/events.d.ts +17 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/file-diff.d.ts +31 -0
- package/dist/file-diff.d.ts.map +1 -0
- package/dist/file-diff.js +196 -0
- package/dist/file-diff.js.map +1 -0
- package/dist/index.d.ts +22 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/mode/agent-step-loop.d.ts +13 -0
- package/dist/mode/agent-step-loop.d.ts.map +1 -0
- package/dist/mode/agent-step-loop.js +578 -0
- package/dist/mode/agent-step-loop.js.map +1 -0
- package/dist/mode/checkpoint-gate.d.ts +25 -0
- package/dist/mode/checkpoint-gate.d.ts.map +1 -0
- package/dist/mode/checkpoint-gate.js +150 -0
- package/dist/mode/checkpoint-gate.js.map +1 -0
- package/dist/mode/checkpoint.d.ts +6 -0
- package/dist/mode/checkpoint.d.ts.map +1 -1
- package/dist/mode/collect-stream.d.ts +2 -0
- package/dist/mode/collect-stream.d.ts.map +1 -1
- package/dist/mode/collect-stream.js +1 -1
- package/dist/mode/collect-stream.js.map +1 -1
- package/dist/mode/project-messages.d.ts.map +1 -1
- package/dist/mode/project-messages.js +9 -67
- package/dist/mode/project-messages.js.map +1 -1
- package/dist/mode/react-loop-options.d.ts +113 -0
- package/dist/mode/react-loop-options.d.ts.map +1 -0
- package/dist/mode/react-loop-options.js +2 -0
- package/dist/mode/react-loop-options.js.map +1 -0
- package/dist/mode/react-loop.d.ts +2 -99
- package/dist/mode/react-loop.d.ts.map +1 -1
- package/dist/mode/react-loop.js +7 -145
- package/dist/mode/react-loop.js.map +1 -1
- package/dist/mode.d.ts +19 -7
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js +1 -1
- package/dist/mode.js.map +1 -1
- package/dist/npm-registry.d.ts +22 -0
- package/dist/npm-registry.d.ts.map +1 -0
- package/dist/npm-registry.js +54 -0
- package/dist/npm-registry.js.map +1 -0
- package/dist/permission.d.ts +31 -0
- package/dist/permission.d.ts.map +1 -1
- package/dist/process-invocation.d.ts +2 -0
- package/dist/process-invocation.d.ts.map +1 -1
- package/dist/process-invocation.js +2 -2
- package/dist/process-invocation.js.map +1 -1
- package/dist/process-tree.d.ts +9 -0
- package/dist/process-tree.d.ts.map +1 -0
- package/dist/process-tree.js +40 -0
- package/dist/process-tree.js.map +1 -0
- package/dist/provider-login-bridge.d.ts +1 -2
- package/dist/provider-login-bridge.d.ts.map +1 -1
- package/dist/provider-login-bridge.js +2 -3
- package/dist/provider-login-bridge.js.map +1 -1
- package/dist/provider.d.ts +409 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/question-draft.d.ts +19 -0
- package/dist/question-draft.d.ts.map +1 -0
- package/dist/question-draft.js +30 -0
- package/dist/question-draft.js.map +1 -0
- package/dist/question.d.ts +204 -0
- package/dist/question.d.ts.map +1 -0
- package/dist/question.js +141 -0
- package/dist/question.js.map +1 -0
- package/dist/questions.d.ts +7 -0
- package/dist/questions.d.ts.map +1 -0
- package/dist/questions.js +7 -0
- package/dist/questions.js.map +1 -0
- package/dist/resolvers.d.ts +14 -1
- package/dist/resolvers.d.ts.map +1 -1
- package/dist/resolvers.js +33 -21
- package/dist/resolvers.js.map +1 -1
- package/dist/schemas.d.ts +53 -53
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -2
- package/dist/schemas.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -1
- package/dist/session-like.d.ts +19 -1
- package/dist/session-like.d.ts.map +1 -1
- package/dist/starting-model.d.ts +12 -0
- package/dist/starting-model.d.ts.map +1 -0
- package/dist/starting-model.js +9 -0
- package/dist/starting-model.js.map +1 -0
- package/dist/tool-dispatch.d.ts.map +1 -1
- package/dist/tool-dispatch.js +10 -4
- package/dist/tool-dispatch.js.map +1 -1
- package/dist/tool-display.d.ts +79 -3
- package/dist/tool-display.d.ts.map +1 -1
- package/dist/tool-display.js +117 -3
- package/dist/tool-display.js.map +1 -1
- package/dist/tool-modes.d.ts +1 -1
- package/dist/tool-modes.d.ts.map +1 -1
- package/dist/tool-modes.js +8 -2
- package/dist/tool-modes.js.map +1 -1
- package/dist/tool-output.d.ts +15 -0
- package/dist/tool-output.d.ts.map +1 -0
- package/dist/tool-output.js +39 -0
- package/dist/tool-output.js.map +1 -0
- package/dist/tool.d.ts +14 -1
- package/dist/tool.d.ts.map +1 -1
- package/dist/tunnel.d.ts +3 -3
- package/dist/tunnel.d.ts.map +1 -1
- package/dist/tunnel.js +2 -2
- package/dist/workflow-approval.d.ts +9 -9
- package/dist/workflow-approval.d.ts.map +1 -1
- package/dist/workflow-approval.js +1 -1
- package/dist/workflow-approval.js.map +1 -1
- package/package.json +37 -5
- package/src/agent-account-limit.test.ts +87 -0
- package/src/agent-account-limit.ts +89 -0
- package/src/agent-computer-use.test.ts +26 -0
- package/src/agent-computer-use.ts +29 -0
- package/src/agent-context-window.ts +51 -0
- package/src/agent-no-reply.ts +16 -0
- package/src/agent-step.ts +26 -0
- package/src/agent-work.ts +102 -0
- package/src/attachment-payload.ts +1 -1
- package/src/auto-approve.test.ts +13 -1
- package/src/auto-approve.ts +19 -0
- package/src/channel-auth.ts +3 -3
- package/src/channel.ts +1 -1
- package/src/client-session.ts +9 -2
- package/src/computer-control.ts +1 -1
- package/src/conversation-history.test.ts +184 -0
- package/src/conversation-history.ts +272 -0
- package/src/cron.test.ts +280 -0
- package/src/cron.ts +355 -0
- package/src/define.test.ts +1 -1
- package/src/define.ts +3 -1
- package/src/event-store.ts +2 -1
- package/src/events.ts +17 -1
- package/src/file-diff.test.ts +103 -0
- package/src/file-diff.ts +208 -0
- package/src/index.ts +121 -1
- package/src/json-file-store.test.ts +1 -1
- package/src/loop-helpers.test.ts +15 -0
- package/src/mode/agent-step-loop.ts +645 -0
- package/src/mode/checkpoint-gate.ts +198 -0
- package/src/mode/checkpoint.ts +6 -0
- package/src/mode/collect-stream.ts +1 -1
- package/src/mode/project-messages.test.ts +11 -0
- package/src/mode/project-messages.ts +8 -61
- package/src/mode/react-loop-options.ts +124 -0
- package/src/mode/react-loop.ts +10 -296
- package/src/mode.ts +19 -7
- package/src/npm-registry.test.ts +95 -0
- package/src/npm-registry.ts +57 -0
- package/src/package-root.test.ts +2 -2
- package/src/permission.ts +30 -0
- package/src/process-invocation.test.ts +25 -0
- package/src/process-invocation.ts +8 -3
- package/src/process-tree.ts +37 -0
- package/src/provider-login-bridge.ts +2 -3
- package/src/provider-schema-constraints.test.ts +1 -1
- package/src/provider-utils.test.ts +1 -1
- package/src/provider.ts +347 -1
- package/src/question-draft.test.ts +57 -0
- package/src/question-draft.ts +47 -0
- package/src/question.test.ts +130 -0
- package/src/question.ts +229 -0
- package/src/questions.ts +6 -0
- package/src/redact-subpath.test.ts +10 -0
- package/src/resolvers.test.ts +84 -1
- package/src/resolvers.ts +40 -27
- package/src/schemas.test.ts +9 -4
- package/src/schemas.ts +3 -2
- package/src/server.ts +3 -0
- package/src/session-like.ts +19 -1
- package/src/skill-mentions.test.ts +1 -1
- package/src/starting-model.test.ts +19 -0
- package/src/starting-model.ts +11 -0
- package/src/token-efficiency.test.ts +1 -1
- package/src/tool-dispatch.ts +10 -4
- package/src/tool-display.test.ts +119 -0
- package/src/tool-display.ts +162 -4
- package/src/tool-gating.test.ts +1 -1
- package/src/tool-icon.test.ts +1 -1
- package/src/tool-modes.test.ts +18 -1
- package/src/tool-modes.ts +6 -2
- package/src/tool-output.test.ts +46 -0
- package/src/tool-output.ts +46 -0
- package/src/tool.ts +14 -1
- package/src/tunnel.ts +3 -3
- package/src/workflow-approval.ts +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { asPluginId } from '../ids.js';
|
|
2
|
+
import type { MoxxyEvent } from '../events.js';
|
|
3
|
+
import type { ModeContext } from '../mode.js';
|
|
4
|
+
import type { StopReason } from '../provider-utils.js';
|
|
5
|
+
import type { CheckpointResult, TurnCheckpoint } from './checkpoint.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The turn-end checkpoint gate and the loop's own lifecycle events, shared by
|
|
9
|
+
* the ReAct loop and the agent-connection loop.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CHECKPOINT_TIMEOUT_MS = 120_000;
|
|
13
|
+
const CHECKPOINT_EVENT_PLUGIN_ID = asPluginId('react-loop');
|
|
14
|
+
|
|
15
|
+
/** Gate outcome: end the turn, or loop again (optionally with a volatile nudge). */
|
|
16
|
+
export type GateVerdict = { readonly kind: 'end' } | { readonly kind: 'loop'; readonly volatileText?: string };
|
|
17
|
+
|
|
18
|
+
export interface GateRound {
|
|
19
|
+
readonly candidateText: string;
|
|
20
|
+
readonly stopReason: StopReason;
|
|
21
|
+
readonly iteration: number;
|
|
22
|
+
readonly consecutiveIdle: number;
|
|
23
|
+
readonly injectionsUsed: number;
|
|
24
|
+
readonly injectionBudget: number;
|
|
25
|
+
readonly maxInjectChars: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function* runCheckpointGate(
|
|
29
|
+
ctx: ModeContext,
|
|
30
|
+
checkpoints: ReadonlyArray<TurnCheckpoint>,
|
|
31
|
+
round: GateRound,
|
|
32
|
+
): AsyncGenerator<MoxxyEvent, GateVerdict> {
|
|
33
|
+
// Natural completions face every checkpoint; truncated/errored candidates
|
|
34
|
+
// only face the idle-tolerant ones — reviewing a half-sentence as if it
|
|
35
|
+
// were a completion claim wastes a checker run and confuses the model.
|
|
36
|
+
const eligible = checkpoints.filter(
|
|
37
|
+
(cp) =>
|
|
38
|
+
(round.stopReason === 'end_turn' || (cp.gateOn ?? 'end_turn') === 'idle') && (cp.applies?.(ctx) ?? true),
|
|
39
|
+
);
|
|
40
|
+
if (eligible.length === 0) return { kind: 'end' };
|
|
41
|
+
|
|
42
|
+
// Budget exhausted → the answer ships as-is, but LOUDLY: silent
|
|
43
|
+
// degradation would let the user believe a gated answer passed its gates.
|
|
44
|
+
if (round.injectionsUsed >= round.injectionBudget) {
|
|
45
|
+
yield await emitError(
|
|
46
|
+
ctx,
|
|
47
|
+
'retryable',
|
|
48
|
+
`checkpoint budget exhausted (${round.injectionBudget} rounds) — ` +
|
|
49
|
+
`ending the turn with unresolved checkpoint feedback; verify the result manually.`,
|
|
50
|
+
);
|
|
51
|
+
return { kind: 'end' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const cp of eligible) {
|
|
55
|
+
// User cancelled mid-gate: end the turn WITHOUT retracting the
|
|
56
|
+
// already-logged answer — an abort must never un-say what was said.
|
|
57
|
+
if (ctx.signal.aborted) return { kind: 'end' };
|
|
58
|
+
|
|
59
|
+
yield await emitCheckpointEvent(ctx, 'checkpoint_started', {
|
|
60
|
+
name: cp.name,
|
|
61
|
+
iteration: round.iteration,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const timeoutMs = Math.max(1_000, cp.timeoutMs ?? DEFAULT_CHECKPOINT_TIMEOUT_MS);
|
|
65
|
+
const timeoutCtl = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => timeoutCtl.abort(), timeoutMs);
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
let result: CheckpointResult;
|
|
69
|
+
try {
|
|
70
|
+
result = await cp.run(
|
|
71
|
+
{
|
|
72
|
+
candidateText: round.candidateText,
|
|
73
|
+
stopReason: round.stopReason,
|
|
74
|
+
iteration: round.iteration,
|
|
75
|
+
consecutiveIdle: round.consecutiveIdle,
|
|
76
|
+
injectionsUsed: round.injectionsUsed,
|
|
77
|
+
injectionBudget: round.injectionBudget,
|
|
78
|
+
signal: AbortSignal.any([ctx.signal, timeoutCtl.signal]),
|
|
79
|
+
},
|
|
80
|
+
ctx,
|
|
81
|
+
);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (ctx.signal.aborted) return { kind: 'end' };
|
|
84
|
+
// Fail OPEN, visibly. A crashed or timed-out checker downgrades the
|
|
85
|
+
// gate to a warning — it must never wedge the turn (fail-closed is a
|
|
86
|
+
// turn that can never end) and never crash it (the answer is already
|
|
87
|
+
// logged).
|
|
88
|
+
const why = timeoutCtl.signal.aborted
|
|
89
|
+
? `timed out after ${timeoutMs}ms`
|
|
90
|
+
: `failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
91
|
+
yield await emitError(ctx, 'retryable', `checkpoint "${cp.name}" ${why} — proceeding unchecked`);
|
|
92
|
+
continue;
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (result.action) {
|
|
98
|
+
case 'pass': {
|
|
99
|
+
yield await emitCheckpointEvent(ctx, 'checkpoint_passed', { name: cp.name });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
case 'stop': {
|
|
103
|
+
// The checkpoint already emitted its own wrap-up events.
|
|
104
|
+
yield await emitCheckpointEvent(ctx, 'checkpoint_stopped', { name: cp.name });
|
|
105
|
+
return { kind: 'end' };
|
|
106
|
+
}
|
|
107
|
+
case 'retry': {
|
|
108
|
+
yield await emitCheckpointEvent(ctx, 'checkpoint_retry', { name: cp.name });
|
|
109
|
+
return { kind: 'loop' };
|
|
110
|
+
}
|
|
111
|
+
case 'inject': {
|
|
112
|
+
// Guard the guard: an inject with blank text would loop the turn
|
|
113
|
+
// with no new signal for the model — a checker bug; fail open.
|
|
114
|
+
const feedback = result.text?.trim();
|
|
115
|
+
if (!feedback) {
|
|
116
|
+
yield await emitError(
|
|
117
|
+
ctx,
|
|
118
|
+
'retryable',
|
|
119
|
+
`checkpoint "${cp.name}" injected empty feedback — ignored`,
|
|
120
|
+
);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const clamped = clampChars(feedback, round.maxInjectChars);
|
|
124
|
+
if (result.volatile) {
|
|
125
|
+
yield await emitCheckpointEvent(ctx, 'checkpoint_injected', {
|
|
126
|
+
name: cp.name,
|
|
127
|
+
volatile: true,
|
|
128
|
+
});
|
|
129
|
+
return { kind: 'loop', volatileText: clamped };
|
|
130
|
+
}
|
|
131
|
+
// Persistent: a checkpoint-origin user prompt. Projected as a
|
|
132
|
+
// user-role message by the existing projection case, marked via the
|
|
133
|
+
// same `origin` machinery trigger prompts use, cache-safe because it
|
|
134
|
+
// is an ordinary append at the log tail.
|
|
135
|
+
yield await ctx.emit({
|
|
136
|
+
type: 'user_prompt',
|
|
137
|
+
sessionId: ctx.sessionId,
|
|
138
|
+
turnId: ctx.turnId,
|
|
139
|
+
source: 'system',
|
|
140
|
+
text: clamped,
|
|
141
|
+
origin: { kind: 'checkpoint', name: cp.name },
|
|
142
|
+
});
|
|
143
|
+
yield await emitCheckpointEvent(ctx, 'checkpoint_injected', {
|
|
144
|
+
name: cp.name,
|
|
145
|
+
volatile: false,
|
|
146
|
+
});
|
|
147
|
+
return { kind: 'loop' };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { kind: 'end' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function clampChars(text: string, max: number): string {
|
|
155
|
+
if (text.length <= max) return text;
|
|
156
|
+
return `${text.slice(0, max)}\n…[checkpoint feedback truncated: ${text.length - max} chars dropped]`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function emitAbort(ctx: ModeContext, reason: string): Promise<MoxxyEvent> {
|
|
160
|
+
return ctx.emit({
|
|
161
|
+
type: 'abort',
|
|
162
|
+
sessionId: ctx.sessionId,
|
|
163
|
+
turnId: ctx.turnId,
|
|
164
|
+
source: 'system',
|
|
165
|
+
reason,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function emitError(
|
|
170
|
+
ctx: ModeContext,
|
|
171
|
+
kind: 'retryable' | 'fatal',
|
|
172
|
+
message: string,
|
|
173
|
+
): Promise<MoxxyEvent> {
|
|
174
|
+
return ctx.emit({
|
|
175
|
+
type: 'error',
|
|
176
|
+
sessionId: ctx.sessionId,
|
|
177
|
+
turnId: ctx.turnId,
|
|
178
|
+
source: 'system',
|
|
179
|
+
kind,
|
|
180
|
+
message,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function emitCheckpointEvent(
|
|
185
|
+
ctx: ModeContext,
|
|
186
|
+
subtype: string,
|
|
187
|
+
payload: unknown,
|
|
188
|
+
): Promise<MoxxyEvent> {
|
|
189
|
+
return ctx.emit({
|
|
190
|
+
type: 'plugin_event',
|
|
191
|
+
sessionId: ctx.sessionId,
|
|
192
|
+
turnId: ctx.turnId,
|
|
193
|
+
source: 'system',
|
|
194
|
+
pluginId: CHECKPOINT_EVENT_PLUGIN_ID,
|
|
195
|
+
subtype,
|
|
196
|
+
payload,
|
|
197
|
+
});
|
|
198
|
+
}
|
package/src/mode/checkpoint.ts
CHANGED
|
@@ -101,6 +101,12 @@ export interface TurnCheckpoint {
|
|
|
101
101
|
* (goal/collab-style stall handling wants these too).
|
|
102
102
|
*/
|
|
103
103
|
readonly gateOn?: 'end_turn' | 'idle';
|
|
104
|
+
/**
|
|
105
|
+
* Also gate turns done by a vendor's agent (an agent connection). Off by
|
|
106
|
+
* default: the agent decides when its turn is done, as in its own tool, so
|
|
107
|
+
* Moxxy's end-of-turn reminders stay out of it.
|
|
108
|
+
*/
|
|
109
|
+
readonly agentConnections?: boolean;
|
|
104
110
|
/**
|
|
105
111
|
* A cheap test of whether this turn needs the checkpoint at all. When it
|
|
106
112
|
* says no, the checkpoint is skipped without a trace — no lifecycle events —
|
|
@@ -22,7 +22,7 @@ export interface CollectedToolUse {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Sum two usage frames so multi-`message_end` responses don't undercount. */
|
|
25
|
-
function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage {
|
|
25
|
+
export function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage {
|
|
26
26
|
const cacheRead =
|
|
27
27
|
a.cacheReadTokens !== undefined || b.cacheReadTokens !== undefined
|
|
28
28
|
? (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0)
|
|
@@ -187,6 +187,17 @@ describe('projectMessagesFromLog tool_result stringify hardening', () => {
|
|
|
187
187
|
it('passes a string output through verbatim', () => {
|
|
188
188
|
expect(toolResultText(logWith('plain text'))).toBe('plain text');
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
it('shows the model every image a result carries, after its text', () => {
|
|
192
|
+
const output = { forModel: 'Two windows', mediaType: 'image/png', base64: 'AAA', moreImages: [{ mediaType: 'image/jpeg', base64: 'BBB' }] };
|
|
193
|
+
const msgs = projectMessagesFromLog({ log: reader(logWith(output)) });
|
|
194
|
+
|
|
195
|
+
expect(msgs.find((m) => m.role === 'tool_result')?.content).toEqual([
|
|
196
|
+
{ type: 'tool_result', toolUseId: 'c1', content: 'Two windows', isError: false },
|
|
197
|
+
{ type: 'image', mediaType: 'image/png', data: 'AAA' },
|
|
198
|
+
{ type: 'image', mediaType: 'image/jpeg', data: 'BBB' },
|
|
199
|
+
]);
|
|
200
|
+
});
|
|
190
201
|
});
|
|
191
202
|
|
|
192
203
|
describe('skillsWithinReach', () => {
|
|
@@ -22,51 +22,7 @@ import {
|
|
|
22
22
|
toolResultStubbed,
|
|
23
23
|
type ElisionState,
|
|
24
24
|
} from '../elision-state.js';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Stringify an arbitrary tool output for the model-facing tool_result text.
|
|
29
|
-
* `tool_result.output` is `unknown` and comes straight from whatever a tool
|
|
30
|
-
* returned, so it can be circular or contain a BigInt — either of which makes
|
|
31
|
-
* `JSON.stringify` THROW. Because projection runs on every request over the
|
|
32
|
-
* append-only log, an unguarded throw here permanently wedges the session
|
|
33
|
-
* (every subsequent re-projection re-throws). Mirrors the try/catch guard the
|
|
34
|
-
* sibling sizing paths (`safeJsonLen`, `toolResultBytes`) already use.
|
|
35
|
-
*/
|
|
36
|
-
function safeStringifyOutput(output: unknown): string {
|
|
37
|
-
if (typeof output === 'string') return output;
|
|
38
|
-
try {
|
|
39
|
-
return JSON.stringify(output ?? '');
|
|
40
|
-
} catch {
|
|
41
|
-
try {
|
|
42
|
-
return String(output ?? '');
|
|
43
|
-
} catch {
|
|
44
|
-
return '[unserializable tool output]';
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* An image ContentBlock extracted from an image-shaped tool output. A tool can
|
|
51
|
-
* return either `{ mediaType, base64 }` (the desktop screenshot/clipboard shape)
|
|
52
|
-
* or the provider-native `{ type:'image', mediaType, data }` — both normalize to
|
|
53
|
-
* the same provider `image` block so the model SEES the pixels instead of a
|
|
54
|
-
* stringified blob of base64.
|
|
55
|
-
*/
|
|
56
|
-
function imageBlockFromOutput(output: unknown): Extract<ContentBlock, { type: 'image' }> | null {
|
|
57
|
-
if (typeof output !== 'object' || output === null) return null;
|
|
58
|
-
const o = output as { type?: unknown; mediaType?: unknown; base64?: unknown; data?: unknown };
|
|
59
|
-
if (typeof o.mediaType !== 'string') return null;
|
|
60
|
-
// `{ mediaType, base64 }` (raw shape) or `{ type:'image', mediaType, data }`.
|
|
61
|
-
const data =
|
|
62
|
-
typeof o.base64 === 'string'
|
|
63
|
-
? o.base64
|
|
64
|
-
: o.type === 'image' && typeof o.data === 'string'
|
|
65
|
-
? o.data
|
|
66
|
-
: null;
|
|
67
|
-
if (data === null) return null;
|
|
68
|
-
return { type: 'image', mediaType: o.mediaType, data };
|
|
69
|
-
}
|
|
25
|
+
import { toolOutputForModel, type ToolImageBlock } from '../tool-output.js';
|
|
70
26
|
|
|
71
27
|
/** Appended to the system prompt while elision is active (see projection). */
|
|
72
28
|
export const ELISION_SYSTEM_NOTE =
|
|
@@ -524,10 +480,10 @@ export function projectMessages(
|
|
|
524
480
|
// Stub bulky old tool output to a recall-able marker (decision shared
|
|
525
481
|
// with estimateContextTokens via toolResultStubbed).
|
|
526
482
|
let text: string;
|
|
527
|
-
// An image-shaped output (a screenshot/clipboard grab) is carried as
|
|
528
|
-
// provider `image`
|
|
483
|
+
// An image-shaped output (a screenshot/clipboard grab) is carried as
|
|
484
|
+
// provider `image` blocks so the model SEES the pixels — only when the
|
|
529
485
|
// result isn't stubbed (elided) or an error and the call succeeded.
|
|
530
|
-
let
|
|
486
|
+
let images: ReadonlyArray<ToolImageBlock> = [];
|
|
531
487
|
// A `recall({ attachment })` result: the original image/document goes
|
|
532
488
|
// beside it, from the prompt that carried it, while the budget allows.
|
|
533
489
|
let restored: ContentBlock | null = null;
|
|
@@ -536,10 +492,6 @@ export function projectMessages(
|
|
|
536
492
|
text = toolResultStubText(e, el);
|
|
537
493
|
} else if (e.error) {
|
|
538
494
|
text = `[error:${e.error.kind}] ${e.error.message}`;
|
|
539
|
-
} else if (isToolDisplayResult(e.output)) {
|
|
540
|
-
// Rich result (e.g. a file diff): the model only needs the short
|
|
541
|
-
// `forModel` summary — the structured `display` is for channels.
|
|
542
|
-
text = e.output.forModel;
|
|
543
495
|
} else if (restoredAtt) {
|
|
544
496
|
const description = (e.output as { forModel?: unknown }).forModel;
|
|
545
497
|
text = typeof description === 'string' ? description : '[attachment restored — see below]';
|
|
@@ -550,20 +502,15 @@ export function projectMessages(
|
|
|
550
502
|
' [It could not be shown again: newer attachments already fill what one request may carry.' +
|
|
551
503
|
' Ask the person to attach it again if you need it now.]';
|
|
552
504
|
}
|
|
553
|
-
} else if (e.ok && (image = imageBlockFromOutput(e.output))) {
|
|
554
|
-
// Short marker satisfies the tool_use→tool_result pairing; the image
|
|
555
|
-
// block (appended below) carries the actual pixels.
|
|
556
|
-
const description = (e.output as { forModel?: unknown }).forModel;
|
|
557
|
-
text = typeof description === 'string'
|
|
558
|
-
? description
|
|
559
|
-
: '[image returned by tool — see attached image]';
|
|
560
505
|
} else {
|
|
561
|
-
|
|
506
|
+
const forModel = toolOutputForModel(e.output);
|
|
507
|
+
text = forModel.text;
|
|
508
|
+
if (e.ok) images = forModel.images;
|
|
562
509
|
}
|
|
563
510
|
const content: ContentBlock[] = [
|
|
564
511
|
{ type: 'tool_result', toolUseId: e.callId, content: text, isError: !e.ok },
|
|
512
|
+
...images,
|
|
565
513
|
];
|
|
566
|
-
if (image) content.push(image);
|
|
567
514
|
if (restored) content.push(restored);
|
|
568
515
|
messages.push({ role: 'tool_result', content });
|
|
569
516
|
recordStable(e.seq);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ModeContext } from '../mode.js';
|
|
2
|
+
import type { AgentGoalStatus, TokenUsage } from '../provider.js';
|
|
3
|
+
import type { StopReason } from '../provider-utils.js';
|
|
4
|
+
import type { StuckLoopReport, StuckTripInfo } from '../tool-dispatch.js';
|
|
5
|
+
import type { TurnCheckpoint } from './checkpoint.js';
|
|
6
|
+
import type { CollectedToolUse } from './collect-stream.js';
|
|
7
|
+
|
|
8
|
+
/** What a clean provider call produced — input to {@link ReactLoopOptions.onProviderSuccess}. */
|
|
9
|
+
export interface ProviderSuccessInfo {
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly stopReason: StopReason;
|
|
12
|
+
readonly toolUses: ReadonlyArray<CollectedToolUse>;
|
|
13
|
+
readonly usage?: TokenUsage;
|
|
14
|
+
readonly iteration: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A completed tool batch — input to {@link ReactLoopOptions.onToolBatchEnd}. */
|
|
18
|
+
export interface ToolBatchInfo {
|
|
19
|
+
readonly toolUses: ReadonlyArray<CollectedToolUse>;
|
|
20
|
+
readonly iteration: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Hooks return this to end the turn; the hook emits its own wrap-up events first. */
|
|
24
|
+
export interface StopDirective {
|
|
25
|
+
readonly action: 'stop';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReactLoopOptions {
|
|
29
|
+
/** Name stamped on `mode_iteration` events (e.g. `'default'`, `'goal'`). */
|
|
30
|
+
readonly strategyName: string;
|
|
31
|
+
/**
|
|
32
|
+
* Iteration cap when the context doesn't supply one. Default 500. May be
|
|
33
|
+
* `Number.POSITIVE_INFINITY` for an uncapped loop (goal mode): the run then
|
|
34
|
+
* ends only via a terminal signal (checkpoint/hook stop, abort, fatal
|
|
35
|
+
* error) — an explicit `ctx.maxIterations` still takes precedence.
|
|
36
|
+
*/
|
|
37
|
+
readonly defaultMaxIterations?: number;
|
|
38
|
+
/** Prefix for provider-error messages (goal mode uses `'goal: '`). */
|
|
39
|
+
readonly errorPrefix?: string;
|
|
40
|
+
/**
|
|
41
|
+
* When set, an already-aborted signal is reported (with this reason) BEFORE
|
|
42
|
+
* the first iteration — goal/collab preflight. When omitted the first
|
|
43
|
+
* iteration's own abort check covers it with the generic reason.
|
|
44
|
+
*/
|
|
45
|
+
readonly preflightAbortReason?: string;
|
|
46
|
+
/** Turn-end gates, run in declared order. Omit/empty → plain ReAct loop. */
|
|
47
|
+
readonly checkpoints?: ReadonlyArray<TurnCheckpoint>;
|
|
48
|
+
/**
|
|
49
|
+
* Hard cap on checkpoint `inject`/`retry` rounds per idle EPISODE — i.e.
|
|
50
|
+
* consecutive gate rounds with no tool work between them; the count resets
|
|
51
|
+
* whenever a tool batch executes (default 3). When exhausted the turn ends
|
|
52
|
+
* with the model's answer as-is plus a visible warning — a
|
|
53
|
+
* permanently-failing gate degrades loudly instead of looping forever.
|
|
54
|
+
*/
|
|
55
|
+
readonly maxInjections?: number;
|
|
56
|
+
/** Clamp on injected feedback length in characters (default 16_384). */
|
|
57
|
+
readonly maxInjectChars?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Stuck-loop policy + wording overrides; sensible defaults from
|
|
60
|
+
* `strategyName`. `action` picks what a detector trip does:
|
|
61
|
+
*
|
|
62
|
+
* - `'abort'` (default): fail the batch and end the turn with a fatal
|
|
63
|
+
* error — the historical behavior, right for attended modes where the
|
|
64
|
+
* user is present to redirect.
|
|
65
|
+
* - `'nudge'`: never stop. The batch still executes (repeated calls are
|
|
66
|
+
* usually legitimate work — re-running a failing build between edits),
|
|
67
|
+
* a visible warning + `extraOnStuck` events are emitted, the detector
|
|
68
|
+
* resets, and `nudgeText` (or a default) rides the next provider call
|
|
69
|
+
* as a volatile steer. For unattended modes (goal) where a heuristic
|
|
70
|
+
* must never kill the run.
|
|
71
|
+
*/
|
|
72
|
+
readonly stuck?: Partial<StuckLoopReport> & {
|
|
73
|
+
readonly action?: 'abort' | 'nudge';
|
|
74
|
+
/** Volatile steer for `'nudge'` trips; default wording when omitted. */
|
|
75
|
+
readonly nudgeText?: (info: StuckTripInfo) => string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Runs after compaction/elision, before the provider call. May return a
|
|
79
|
+
* volatile user message to ride ONLY the next call (collab's inbox/pause
|
|
80
|
+
* awareness) — never appended to the log; the cache strategy is told via
|
|
81
|
+
* `volatileTailCount` so its tail breakpoint stays ahead of it.
|
|
82
|
+
*/
|
|
83
|
+
readonly onIterationStart?: (
|
|
84
|
+
ctx: ModeContext,
|
|
85
|
+
iteration: number,
|
|
86
|
+
) => Promise<{ readonly volatileUserText?: string } | undefined>;
|
|
87
|
+
/**
|
|
88
|
+
* Runs after every clean provider call (reasoning already logged, tools not
|
|
89
|
+
* yet executed). Return `{action: 'stop'}` to end the turn — the hook emits
|
|
90
|
+
* its own wrap-up events first (goal mode's token-budget backstop).
|
|
91
|
+
*/
|
|
92
|
+
readonly onProviderSuccess?: (
|
|
93
|
+
ctx: ModeContext,
|
|
94
|
+
info: ProviderSuccessInfo,
|
|
95
|
+
) => Promise<StopDirective | undefined>;
|
|
96
|
+
/**
|
|
97
|
+
* Runs after a tool batch executes cleanly. Return `{action: 'stop'}` to
|
|
98
|
+
* end the turn (goal/collab terminal-tool detection: `goal_complete`,
|
|
99
|
+
* `collab_done`); the hook emits its own completion events first.
|
|
100
|
+
*/
|
|
101
|
+
readonly onToolBatchEnd?: (
|
|
102
|
+
ctx: ModeContext,
|
|
103
|
+
info: ToolBatchInfo,
|
|
104
|
+
) => Promise<StopDirective | undefined>;
|
|
105
|
+
/**
|
|
106
|
+
* Runs after an agent connection's turn that reported the state of the
|
|
107
|
+
* agent's own goal. Return `{action: 'stop'}` to end the turn (goal mode
|
|
108
|
+
* wraps up a met goal); the hook emits its own completion events first.
|
|
109
|
+
*/
|
|
110
|
+
readonly onAgentGoal?: (ctx: ModeContext, status: AgentGoalStatus) => Promise<StopDirective | undefined>;
|
|
111
|
+
/**
|
|
112
|
+
* Agent connections only: the agent finished its own plan (Markdown). Plan
|
|
113
|
+
* mode shows it as its plan card and ends the turn; without this hook the
|
|
114
|
+
* plan is shown as the agent's message.
|
|
115
|
+
*/
|
|
116
|
+
readonly onAgentPlan?: (ctx: ModeContext, plan: string) => Promise<StopDirective | undefined>;
|
|
117
|
+
/**
|
|
118
|
+
* Replaces the default "exceeded maxIterations" fatal error — the hook
|
|
119
|
+
* emits its own cap-reached events (goal mode adds a plugin event and its
|
|
120
|
+
* own wording). The loop returns right after.
|
|
121
|
+
*/
|
|
122
|
+
readonly onMaxIterations?: (ctx: ModeContext, maxIterations: number) => Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
|