@revealui/ai 1.1.0 → 1.2.2
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/README.md +22 -0
- package/dist/orchestration/loop-guard.d.ts +85 -0
- package/dist/orchestration/loop-guard.d.ts.map +1 -0
- package/dist/orchestration/loop-guard.js +252 -0
- package/dist/orchestration/runtime.d.ts +35 -1
- package/dist/orchestration/runtime.d.ts.map +1 -1
- package/dist/orchestration/runtime.js +115 -8
- package/dist/orchestration/streaming-runtime.d.ts.map +1 -1
- package/dist/orchestration/streaming-runtime.js +21 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -357,6 +357,28 @@ import { AgentRuntime } from '@revealui/ai/orchestration/runtime'
|
|
|
357
357
|
const runtime = new AgentRuntime()
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
+
#### Intentional dual: interactive runtime and governed receipts
|
|
361
|
+
|
|
362
|
+
`AgentRuntime.executeTask` and `StreamingAgentRuntime.streamTask` are the interactive product loops. `runGovernedTask` (Apify actor `@revealui/apify-actor-governed-run`) is the governed receipt loop. Both call tools in a loop. They stay separate: receipts need an ordered action log plus charge and step caps; the interactive runtimes stay open until the model finishes, the iteration cap, or Studio LoopGuard says the loop is not advancing. Do not merge them.
|
|
363
|
+
|
|
364
|
+
#### Studio LoopGuard (GAP-362)
|
|
365
|
+
|
|
366
|
+
When the RevDev daemon socket is reachable (`REVEALUI_SOCKET`, otherwise `~/.local/share/revealui/harness.sock`), both interactive runtimes call `loop.arm` once per task and `loop.tick` after each model iteration. `loop.status` reads the loop. This package does not call `loop.stop`. `session.end` and `harness.prune` reap loops on the daemon (`packages/ai/src/orchestration/loop-guard.ts:4-12`).
|
|
367
|
+
|
|
368
|
+
Fail-open: a missing socket returns immediately. A socket that refuses the connection or never answers is bounded by `timeoutMs` (default 750) and then ignored. The task does not throw and does not change its result.
|
|
369
|
+
|
|
370
|
+
An iteration is advancing when the model returns a final answer, or when at least one tool runs that was not a duplicate of an earlier call in the same task (`packages/ai/src/orchestration/loop-guard.ts:132`). Duplicate-only rounds send `advanced: false`.
|
|
371
|
+
|
|
372
|
+
No-op limit: omit `noopLimit` and the daemon applies 3 (`DAEMON_LOOP_NOOP_LIMIT`, RevDev `DEFAULT_LOOP_NOOP_LIMIT`) (`packages/ai/src/orchestration/loop-guard.ts:27`). This package does not keep a second counter. When `loop.tick` returns `stop: true` or `status: not_advancing`, the interactive runtime stops and surfaces `lastSignal` (`packages/ai/src/orchestration/loop-guard.ts:239`). `intervalMs` defaults to 60000 so an interactive tool loop is not flagged as a sub-minute idle poll (`packages/ai/src/orchestration/loop-guard.ts:34`). The runtime does not sleep for that interval.
|
|
373
|
+
|
|
374
|
+
The actor id is the cached Studio session (`daemon-sessions/<pid>.id`, then `<ppid>.id`) when one exists, otherwise `revealui-product-runtime`.
|
|
375
|
+
|
|
376
|
+
`runGovernedTask` does not call LoopGuard. Do not merge it with `AgentRuntime`.
|
|
377
|
+
|
|
378
|
+
Do not boot the MCP Hypervisor silent process health loop without a WIRE mount and a credential owner (`packages/mcp/src/hypervisor.ts:999`).
|
|
379
|
+
|
|
380
|
+
Pass `loopGuard: false` only when a caller must not report, including tests. The default wires whenever the daemon answers.
|
|
381
|
+
|
|
360
382
|
### Tools
|
|
361
383
|
|
|
362
384
|
Tool registry and execution system with MCP integration.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio LoopGuard client (GAP-362).
|
|
3
|
+
*
|
|
4
|
+
* Sock methods on harness.sock are `loop.arm`, `loop.tick`, and `loop.status`.
|
|
5
|
+
* This client does not call `loop.stop`. `session.end` and `harness.prune`
|
|
6
|
+
* reap loops on the daemon. Product paths with no daemon keep today's
|
|
7
|
+
* behavior: a missing socket returns immediately, and a dead or silent socket
|
|
8
|
+
* fails open inside `timeoutMs` without throwing.
|
|
9
|
+
*
|
|
10
|
+
* The daemon owns the no-op counter. Omit `noopLimit` and it applies
|
|
11
|
+
* `DAEMON_LOOP_NOOP_LIMIT` (3, RevDev `DEFAULT_LOOP_NOOP_LIMIT`). After that
|
|
12
|
+
* many consecutive `advanced: false` ticks, `loop.tick` returns `stop: true`
|
|
13
|
+
* and `status: not_advancing`. This module does not keep a second counter.
|
|
14
|
+
*
|
|
15
|
+
* `AgentRuntime` stays separate from `runGovernedTask`. Receipts are not
|
|
16
|
+
* this loop. Do not boot the MCP Hypervisor silent process health loop
|
|
17
|
+
* without a WIRE mount and a credential owner.
|
|
18
|
+
*/
|
|
19
|
+
/** RevDev `DEFAULT_LOOP_NOOP_LIMIT`. Omitted on arm so the daemon applies it. */
|
|
20
|
+
export declare const DAEMON_LOOP_NOOP_LIMIT = 3;
|
|
21
|
+
/**
|
|
22
|
+
* Cadence declaration sent on `loop.arm`. RevDev warns on idle intervals
|
|
23
|
+
* under 60s. Interactive tool loops are not idle polls, so the declaration
|
|
24
|
+
* stays at this floor. The runtime does not sleep for this long.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DAEMON_LOOP_INTERVAL_MS = 60000;
|
|
27
|
+
/** Bound for a socket that exists but never answers. */
|
|
28
|
+
export declare const DAEMON_LOOP_TIMEOUT_MS = 750;
|
|
29
|
+
/** Used when no Studio session id is cached. Unknown ids are daemon-minted. */
|
|
30
|
+
export declare const PRODUCT_RUNTIME_ACTOR_ID = "revealui-product-runtime";
|
|
31
|
+
export type LoopGuardStatus = 'armed' | 'not_advancing' | 'stopped' | 'unavailable';
|
|
32
|
+
export interface LoopGuardSignal {
|
|
33
|
+
readonly attached: boolean;
|
|
34
|
+
readonly status: LoopGuardStatus;
|
|
35
|
+
readonly signal: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface LoopGuardTickInput {
|
|
38
|
+
readonly advanced: boolean;
|
|
39
|
+
readonly tokensIn?: number;
|
|
40
|
+
readonly tokensOut?: number;
|
|
41
|
+
readonly costMicros?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface LoopGuardPort {
|
|
44
|
+
arm(input: {
|
|
45
|
+
readonly loopId: string;
|
|
46
|
+
readonly actorAgentId?: string;
|
|
47
|
+
}): Promise<LoopGuardSignal>;
|
|
48
|
+
tick(input: {
|
|
49
|
+
readonly loopId: string;
|
|
50
|
+
} & LoopGuardTickInput): Promise<LoopGuardSignal>;
|
|
51
|
+
/** Read the loop. `{ loop: null }` from the daemon is not attached. */
|
|
52
|
+
status(loopId: string): Promise<LoopGuardSignal>;
|
|
53
|
+
}
|
|
54
|
+
export interface DaemonLoopGuardConfig {
|
|
55
|
+
/** Defaults to `REVEALUI_SOCKET` or `~/.local/share/revealui/harness.sock`. */
|
|
56
|
+
readonly socketPath?: string;
|
|
57
|
+
/** RPC budget per call when the socket accepts but stays silent. */
|
|
58
|
+
readonly timeoutMs?: number;
|
|
59
|
+
/** `loop.arm` cadence declaration. Defaults to {@link DAEMON_LOOP_INTERVAL_MS}. */
|
|
60
|
+
readonly intervalMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Sent only when set. Omitted so the daemon keeps {@link DAEMON_LOOP_NOOP_LIMIT}.
|
|
63
|
+
*/
|
|
64
|
+
readonly noopLimit?: number;
|
|
65
|
+
/** Overrides the cached Studio session id and the product fallback. */
|
|
66
|
+
readonly actorAgentId?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function defaultHarnessSocketPath(): string;
|
|
69
|
+
export declare function isHarnessSocketPresent(socketPath?: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Studio session id written by harness SessionStart.
|
|
72
|
+
* The hook process records the parent pid; in-process callers record ppid.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readStudioActorId(): string | null;
|
|
75
|
+
export declare function studioLoopId(taskId: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* An iteration advances when the model finishes the task or runs at least
|
|
78
|
+
* one tool that was not a duplicate of an earlier call in the same task.
|
|
79
|
+
*/
|
|
80
|
+
export declare function iterationAdvanced(input: {
|
|
81
|
+
readonly completed: boolean;
|
|
82
|
+
readonly newToolExecutions: number;
|
|
83
|
+
}): boolean;
|
|
84
|
+
export declare function createDaemonLoopGuard(config?: DaemonLoopGuardConfig): LoopGuardPort;
|
|
85
|
+
//# sourceMappingURL=loop-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-guard.d.ts","sourceRoot":"","sources":["../../src/orchestration/loop-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAQH,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAInE,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,eAAe,GAAG,SAAS,GAAG,aAAa,CAAC;AAEpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAClG,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACxF,uEAAuE;IACvE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;OAEG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAQD,wBAAgB,wBAAwB,IAAI,MAAM,CAIjD;AAED,wBAAgB,sBAAsB,CAAC,UAAU,GAAE,MAAmC,GAAG,OAAO,CAO/F;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAejD;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC,GAAG,OAAO,CAEV;AAED,wBAAgB,qBAAqB,CAAC,MAAM,GAAE,qBAA0B,GAAG,aAAa,CA6EvF"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio LoopGuard client (GAP-362).
|
|
3
|
+
*
|
|
4
|
+
* Sock methods on harness.sock are `loop.arm`, `loop.tick`, and `loop.status`.
|
|
5
|
+
* This client does not call `loop.stop`. `session.end` and `harness.prune`
|
|
6
|
+
* reap loops on the daemon. Product paths with no daemon keep today's
|
|
7
|
+
* behavior: a missing socket returns immediately, and a dead or silent socket
|
|
8
|
+
* fails open inside `timeoutMs` without throwing.
|
|
9
|
+
*
|
|
10
|
+
* The daemon owns the no-op counter. Omit `noopLimit` and it applies
|
|
11
|
+
* `DAEMON_LOOP_NOOP_LIMIT` (3, RevDev `DEFAULT_LOOP_NOOP_LIMIT`). After that
|
|
12
|
+
* many consecutive `advanced: false` ticks, `loop.tick` returns `stop: true`
|
|
13
|
+
* and `status: not_advancing`. This module does not keep a second counter.
|
|
14
|
+
*
|
|
15
|
+
* `AgentRuntime` stays separate from `runGovernedTask`. Receipts are not
|
|
16
|
+
* this loop. Do not boot the MCP Hypervisor silent process health loop
|
|
17
|
+
* without a WIRE mount and a credential owner.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
21
|
+
import { createConnection } from 'node:net';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
/** RevDev `DEFAULT_LOOP_NOOP_LIMIT`. Omitted on arm so the daemon applies it. */
|
|
25
|
+
export const DAEMON_LOOP_NOOP_LIMIT = 3;
|
|
26
|
+
/**
|
|
27
|
+
* Cadence declaration sent on `loop.arm`. RevDev warns on idle intervals
|
|
28
|
+
* under 60s. Interactive tool loops are not idle polls, so the declaration
|
|
29
|
+
* stays at this floor. The runtime does not sleep for this long.
|
|
30
|
+
*/
|
|
31
|
+
export const DAEMON_LOOP_INTERVAL_MS = 60_000;
|
|
32
|
+
/** Bound for a socket that exists but never answers. */
|
|
33
|
+
export const DAEMON_LOOP_TIMEOUT_MS = 750;
|
|
34
|
+
/** Used when no Studio session id is cached. Unknown ids are daemon-minted. */
|
|
35
|
+
export const PRODUCT_RUNTIME_ACTOR_ID = 'revealui-product-runtime';
|
|
36
|
+
const ACTOR_ID_MAX = 256;
|
|
37
|
+
const UNAVAILABLE = {
|
|
38
|
+
attached: false,
|
|
39
|
+
status: 'unavailable',
|
|
40
|
+
signal: null,
|
|
41
|
+
};
|
|
42
|
+
export function defaultHarnessSocketPath() {
|
|
43
|
+
return (process.env.REVEALUI_SOCKET ?? join(homedir(), '.local', 'share', 'revealui', 'harness.sock'));
|
|
44
|
+
}
|
|
45
|
+
export function isHarnessSocketPresent(socketPath = defaultHarnessSocketPath()) {
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(socketPath))
|
|
48
|
+
return false;
|
|
49
|
+
return Boolean(statSync(socketPath));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Studio session id written by harness SessionStart.
|
|
57
|
+
* The hook process records the parent pid; in-process callers record ppid.
|
|
58
|
+
*/
|
|
59
|
+
export function readStudioActorId() {
|
|
60
|
+
const dir = process.env.REVDEV_DAEMON_SESSION_DIR ??
|
|
61
|
+
join(homedir(), '.local', 'share', 'revealui', 'daemon-sessions');
|
|
62
|
+
const keys = [process.pid, process.ppid];
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(join(dir, `${key}.id`), 'utf8');
|
|
66
|
+
const id = usableActorId(raw);
|
|
67
|
+
if (id)
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* absent */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
export function studioLoopId(taskId) {
|
|
77
|
+
const safe = taskId.length > 120 ? taskId.slice(0, 120) : taskId;
|
|
78
|
+
return `agent-runtime:${safe}:${randomUUID()}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* An iteration advances when the model finishes the task or runs at least
|
|
82
|
+
* one tool that was not a duplicate of an earlier call in the same task.
|
|
83
|
+
*/
|
|
84
|
+
export function iterationAdvanced(input) {
|
|
85
|
+
return input.completed || input.newToolExecutions > 0;
|
|
86
|
+
}
|
|
87
|
+
export function createDaemonLoopGuard(config = {}) {
|
|
88
|
+
const socketPath = config.socketPath ?? defaultHarnessSocketPath();
|
|
89
|
+
const timeoutMs = config.timeoutMs ?? DAEMON_LOOP_TIMEOUT_MS;
|
|
90
|
+
const intervalMs = config.intervalMs ?? DAEMON_LOOP_INTERVAL_MS;
|
|
91
|
+
const noopLimit = config.noopLimit;
|
|
92
|
+
let attached = false;
|
|
93
|
+
let disabled = false;
|
|
94
|
+
let actorAgentId = usableActorId(config.actorAgentId ?? '') ?? PRODUCT_RUNTIME_ACTOR_ID;
|
|
95
|
+
async function call(method, params) {
|
|
96
|
+
return rpcCall(socketPath, method, params, timeoutMs);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
async arm(input) {
|
|
100
|
+
if (disabled)
|
|
101
|
+
return UNAVAILABLE;
|
|
102
|
+
const explicit = usableActorId(input.actorAgentId ?? config.actorAgentId ?? '');
|
|
103
|
+
actorAgentId = explicit ?? readStudioActorId() ?? PRODUCT_RUNTIME_ACTOR_ID;
|
|
104
|
+
if (!isHarnessSocketPresent(socketPath)) {
|
|
105
|
+
disabled = true;
|
|
106
|
+
return UNAVAILABLE;
|
|
107
|
+
}
|
|
108
|
+
const params = {
|
|
109
|
+
loopId: input.loopId,
|
|
110
|
+
intervalMs,
|
|
111
|
+
actorAgentId,
|
|
112
|
+
};
|
|
113
|
+
if (typeof noopLimit === 'number' && noopLimit > 0) {
|
|
114
|
+
params.noopLimit = Math.floor(noopLimit);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const result = await call('loop.arm', params);
|
|
118
|
+
attached = true;
|
|
119
|
+
return signalFromResult(result);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
disabled = true;
|
|
123
|
+
attached = false;
|
|
124
|
+
return UNAVAILABLE;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async tick(input) {
|
|
128
|
+
if (!attached || disabled)
|
|
129
|
+
return UNAVAILABLE;
|
|
130
|
+
const params = {
|
|
131
|
+
loopId: input.loopId,
|
|
132
|
+
advanced: input.advanced,
|
|
133
|
+
actorAgentId,
|
|
134
|
+
};
|
|
135
|
+
if (typeof input.tokensIn === 'number' && input.tokensIn > 0) {
|
|
136
|
+
params.tokensIn = Math.floor(input.tokensIn);
|
|
137
|
+
}
|
|
138
|
+
if (typeof input.tokensOut === 'number' && input.tokensOut > 0) {
|
|
139
|
+
params.tokensOut = Math.floor(input.tokensOut);
|
|
140
|
+
}
|
|
141
|
+
if (typeof input.costMicros === 'number' && input.costMicros > 0) {
|
|
142
|
+
params.costMicros = Math.floor(input.costMicros);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const result = await call('loop.tick', params);
|
|
146
|
+
return signalFromResult(result);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
disabled = true;
|
|
150
|
+
return UNAVAILABLE;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
async status(loopId) {
|
|
154
|
+
if (!attached || disabled)
|
|
155
|
+
return UNAVAILABLE;
|
|
156
|
+
try {
|
|
157
|
+
const result = await call('loop.status', { loopId, actorAgentId });
|
|
158
|
+
return signalFromResult(result);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
disabled = true;
|
|
162
|
+
return UNAVAILABLE;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function usableActorId(raw) {
|
|
168
|
+
const id = raw.trim();
|
|
169
|
+
if (id.length === 0 || id.length > ACTOR_ID_MAX)
|
|
170
|
+
return null;
|
|
171
|
+
if (id.includes('/') || id.includes('\\') || id.includes('\0'))
|
|
172
|
+
return null;
|
|
173
|
+
return id;
|
|
174
|
+
}
|
|
175
|
+
function signalFromResult(result) {
|
|
176
|
+
if (!result || typeof result !== 'object') {
|
|
177
|
+
return { attached: true, status: 'armed', signal: null };
|
|
178
|
+
}
|
|
179
|
+
const record = result;
|
|
180
|
+
if (record.loop == null) {
|
|
181
|
+
return UNAVAILABLE;
|
|
182
|
+
}
|
|
183
|
+
if (typeof record.loop !== 'object') {
|
|
184
|
+
return { attached: true, status: 'armed', signal: null };
|
|
185
|
+
}
|
|
186
|
+
const statusRaw = record.loop.status;
|
|
187
|
+
const signalRaw = record.loop.lastSignal;
|
|
188
|
+
const signal = typeof signalRaw === 'string' ? signalRaw : null;
|
|
189
|
+
if (record.stop === true || statusRaw === 'not_advancing') {
|
|
190
|
+
return {
|
|
191
|
+
attached: true,
|
|
192
|
+
status: 'not_advancing',
|
|
193
|
+
signal: signal ?? 'loop not advancing',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (statusRaw === 'stopped' || statusRaw === 'armed') {
|
|
197
|
+
return { attached: true, status: statusRaw, signal };
|
|
198
|
+
}
|
|
199
|
+
return { attached: true, status: 'armed', signal };
|
|
200
|
+
}
|
|
201
|
+
function rpcCall(socketPath, method, params, timeoutMs) {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const socket = createConnection(socketPath);
|
|
204
|
+
let buffer = '';
|
|
205
|
+
let settled = false;
|
|
206
|
+
let timer;
|
|
207
|
+
const finish = (err, value) => {
|
|
208
|
+
if (settled)
|
|
209
|
+
return;
|
|
210
|
+
settled = true;
|
|
211
|
+
if (timer)
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
socket.destroy();
|
|
214
|
+
if (err)
|
|
215
|
+
reject(err);
|
|
216
|
+
else
|
|
217
|
+
resolve(value);
|
|
218
|
+
};
|
|
219
|
+
timer = setTimeout(() => {
|
|
220
|
+
finish(new Error(`loop guard RPC timeout after ${timeoutMs}ms`));
|
|
221
|
+
}, timeoutMs);
|
|
222
|
+
socket.on('connect', () => {
|
|
223
|
+
const frame = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
|
|
224
|
+
socket.write(`${frame}\n`);
|
|
225
|
+
});
|
|
226
|
+
socket.on('data', (chunk) => {
|
|
227
|
+
buffer += chunk.toString();
|
|
228
|
+
const lines = buffer.split('\n');
|
|
229
|
+
buffer = lines.pop() ?? '';
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
if (!line.trim())
|
|
232
|
+
continue;
|
|
233
|
+
try {
|
|
234
|
+
const resp = JSON.parse(line);
|
|
235
|
+
if (resp.error) {
|
|
236
|
+
finish(new Error(resp.error.message ?? 'loop guard RPC error'));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
finish(null, resp.result);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
finish(err instanceof Error ? err : new Error(String(err)));
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
socket.on('error', (err) => {
|
|
249
|
+
finish(err);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent Runtime
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Interactive tool loop (`executeTask`). Intentionally separate from
|
|
5
|
+
* `runGovernedTask` (governed receipts in `@revealui/apify-actor-governed-run`):
|
|
6
|
+
* receipts need an ordered action log and hard step caps; this runtime stays
|
|
7
|
+
* an open loop. Do not merge them.
|
|
8
|
+
*
|
|
9
|
+
* When the RevDev harness socket is reachable, each task arms Studio LoopGuard
|
|
10
|
+
* and ticks once per model iteration (`loop.arm`, `loop.tick`, `loop.status`).
|
|
11
|
+
* This runtime does not call `loop.stop`. `session.end` and `harness.prune`
|
|
12
|
+
* reap the loop. No daemon means no tick and no throw.
|
|
13
|
+
*
|
|
14
|
+
* Do not boot the MCP Hypervisor silent process health loop without a WIRE
|
|
15
|
+
* mount and a credential owner.
|
|
5
16
|
*/
|
|
6
17
|
import type { LLMClient } from '../llm/client.js';
|
|
7
18
|
import type { ReasoningEffort } from '../llm/providers/base.js';
|
|
@@ -10,6 +21,9 @@ import type { ApprovalCallback } from '../tools/base.js';
|
|
|
10
21
|
import { type McpClientLike } from '../tools/mcp-adapter.js';
|
|
11
22
|
import type { McpToolCallEvent } from '../tools/mcp-events.js';
|
|
12
23
|
import type { Agent, AgentResult, Task } from './agent.js';
|
|
24
|
+
import { type LoopGuardPort, type LoopGuardTickInput } from './loop-guard.js';
|
|
25
|
+
export type { LoopGuardPort, LoopGuardSignal, LoopGuardTickInput } from './loop-guard.js';
|
|
26
|
+
export { createDaemonLoopGuard, DAEMON_LOOP_INTERVAL_MS, DAEMON_LOOP_NOOP_LIMIT, DAEMON_LOOP_TIMEOUT_MS, iterationAdvanced, PRODUCT_RUNTIME_ACTOR_ID, studioLoopId, } from './loop-guard.js';
|
|
13
27
|
/**
|
|
14
28
|
* Reasoning-depth hint for a task. Alias of the neutral `ReasoningEffort` — the agnostic
|
|
15
29
|
* Reasoner port's control vocabulary. Mapping a level to a provider's native control
|
|
@@ -89,9 +103,19 @@ export interface RuntimeConfig {
|
|
|
89
103
|
* policy (e.g., from AgentSecuritySchema.requiresHumanApproval).
|
|
90
104
|
*/
|
|
91
105
|
alwaysRequireApproval?: string[];
|
|
106
|
+
/**
|
|
107
|
+
* Studio LoopGuard (GAP-362). Default: `loop.arm`, `loop.tick`, and
|
|
108
|
+
* `loop.status` on the RevDev harness socket when it is reachable.
|
|
109
|
+
* This runtime does not call `loop.stop`. `session.end` and `harness.prune`
|
|
110
|
+
* reap the loop. Missing socket, connect failure, and RPC timeout leave
|
|
111
|
+
* the task on the unwired path (no throw). Pass `false` to skip the daemon
|
|
112
|
+
* even when Studio is attached. `runGovernedTask` does not use this port.
|
|
113
|
+
*/
|
|
114
|
+
loopGuard?: LoopGuardPort | false;
|
|
92
115
|
}
|
|
93
116
|
export declare class AgentRuntime {
|
|
94
117
|
protected config: RuntimeConfig;
|
|
118
|
+
private readonly loopGuard;
|
|
95
119
|
private taskQueue;
|
|
96
120
|
private executingTasks;
|
|
97
121
|
private isShuttingDown;
|
|
@@ -101,6 +125,16 @@ export declare class AgentRuntime {
|
|
|
101
125
|
*/
|
|
102
126
|
executeTask(agent: Agent, task: Task, llmClient: LLMClient): Promise<AgentResult>;
|
|
103
127
|
private runTask;
|
|
128
|
+
/**
|
|
129
|
+
* Arm LoopGuard for this task when Studio's daemon answers.
|
|
130
|
+
* Returns null when the daemon is absent or the call fails open.
|
|
131
|
+
*/
|
|
132
|
+
protected openStudioLoop(taskId: string): Promise<string | null>;
|
|
133
|
+
/**
|
|
134
|
+
* Report one model iteration. A non-null return is the daemon stop signal
|
|
135
|
+
* (`not_advancing` after the daemon no-op limit, default 3).
|
|
136
|
+
*/
|
|
137
|
+
protected tickStudioLoop(loopId: string | null, tick: LoopGuardTickInput): Promise<string | null>;
|
|
104
138
|
/**
|
|
105
139
|
* Add task to queue
|
|
106
140
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/orchestration/runtime.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/orchestration/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAwB,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAGtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,+CAA+C,CAAC;AACxF,OAAO,KAAK,EAAE,gBAAgB,EAAoB,MAAM,kBAAkB,CAAC;AAE3E,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EAExB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,eAAe,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IACpE;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE;;;;OAIG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,gCAAgC,EAAE,SAAS,CAAC;IAC/D;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,aAAa,GAAG,KAAK,CAAC;CACnC;AAED,qBAAa,YAAY;IACvB,SAAS,CAAC,MAAM,EAAE,aAAa,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuB;IACjD,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,cAAc,CAAgD;IACtE,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,GAAE,aAAkB;IA4BtC;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;YAqBzE,OAAO;IAiRrB;;;OAGG;cACa,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAWtE;;;OAGG;cACa,cAAc,CAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAczB;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAI7B;;OAEG;IACG,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAc9E;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B9B;;OAEG;IACH,SAAS,IAAI;QACX,cAAc,EAAE,OAAO,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,cAAc,EAAE,MAAM,CAAC;KACxB;CAOF"}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent Runtime
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Interactive tool loop (`executeTask`). Intentionally separate from
|
|
5
|
+
* `runGovernedTask` (governed receipts in `@revealui/apify-actor-governed-run`):
|
|
6
|
+
* receipts need an ordered action log and hard step caps; this runtime stays
|
|
7
|
+
* an open loop. Do not merge them.
|
|
8
|
+
*
|
|
9
|
+
* When the RevDev harness socket is reachable, each task arms Studio LoopGuard
|
|
10
|
+
* and ticks once per model iteration (`loop.arm`, `loop.tick`, `loop.status`).
|
|
11
|
+
* This runtime does not call `loop.stop`. `session.end` and `harness.prune`
|
|
12
|
+
* reap the loop. No daemon means no tick and no throw.
|
|
13
|
+
*
|
|
14
|
+
* Do not boot the MCP Hypervisor silent process health loop without a WIRE
|
|
15
|
+
* mount and a credential owner.
|
|
5
16
|
*/
|
|
6
17
|
import { registerCleanupHandler } from '@revealui/core/monitoring';
|
|
7
18
|
import { estimateCost } from '../llm/token-counter.js';
|
|
@@ -9,12 +20,17 @@ import { toolParametersToJsonSchema } from '../llm/tool-json-schema.js';
|
|
|
9
20
|
import { ToolCallDeduplicator } from '../tools/deduplicator.js';
|
|
10
21
|
import { createToolsFromMcpClient } from '../tools/mcp-adapter.js';
|
|
11
22
|
import { createWebSearchTool } from '../tools/web/duck-duck-go.js';
|
|
23
|
+
import { createDaemonLoopGuard, iterationAdvanced, studioLoopId, } from './loop-guard.js';
|
|
24
|
+
export { createDaemonLoopGuard, DAEMON_LOOP_INTERVAL_MS, DAEMON_LOOP_NOOP_LIMIT, DAEMON_LOOP_TIMEOUT_MS, iterationAdvanced, PRODUCT_RUNTIME_ACTOR_ID, studioLoopId, } from './loop-guard.js';
|
|
12
25
|
export class AgentRuntime {
|
|
13
26
|
config;
|
|
27
|
+
loopGuard;
|
|
14
28
|
taskQueue = [];
|
|
15
29
|
executingTasks = new Map();
|
|
16
30
|
isShuttingDown = false;
|
|
17
31
|
constructor(config = {}) {
|
|
32
|
+
this.loopGuard =
|
|
33
|
+
config.loopGuard === false ? null : (config.loopGuard ?? createDaemonLoopGuard());
|
|
18
34
|
this.config = {
|
|
19
35
|
maxIterations: config.maxIterations ?? 10,
|
|
20
36
|
timeout: config.timeout ?? 60000, // 60 seconds
|
|
@@ -60,6 +76,7 @@ export class AgentRuntime {
|
|
|
60
76
|
let iterations = 0;
|
|
61
77
|
let totalTokens = 0;
|
|
62
78
|
let totalCostUsd = 0;
|
|
79
|
+
const loopId = await this.openStudioLoop(task.id);
|
|
63
80
|
// Merge MCP-discovered tools into the agent's tool set via `McpClient`.
|
|
64
81
|
const mcpTools = [];
|
|
65
82
|
const onToolAudit = this.config.onToolAudit;
|
|
@@ -138,12 +155,9 @@ export class AgentRuntime {
|
|
|
138
155
|
const iterationTokens = response.usage?.totalTokens ?? 0;
|
|
139
156
|
totalTokens += iterationTokens;
|
|
140
157
|
const model = this.config.model;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
totalCostUsd +=
|
|
145
|
-
estimateCost(inputTokens, model, 'input').estimatedCostUsd +
|
|
146
|
-
estimateCost(outputTokens, model, 'output').estimatedCostUsd;
|
|
158
|
+
const spend = iterationSpend(response, model, iterationTokens);
|
|
159
|
+
if (spend.costUsd > 0) {
|
|
160
|
+
totalCostUsd += spend.costUsd;
|
|
147
161
|
}
|
|
148
162
|
// Add assistant response to messages
|
|
149
163
|
messages.push({
|
|
@@ -153,6 +167,22 @@ export class AgentRuntime {
|
|
|
153
167
|
});
|
|
154
168
|
// If no tool calls, task is complete
|
|
155
169
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
170
|
+
const stopReason = await this.tickStudioLoop(loopId, {
|
|
171
|
+
advanced: iterationAdvanced({ completed: true, newToolExecutions: 0 }),
|
|
172
|
+
...spend.tick,
|
|
173
|
+
});
|
|
174
|
+
if (stopReason) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: stopReason,
|
|
178
|
+
toolResults,
|
|
179
|
+
metadata: {
|
|
180
|
+
executionTime: Date.now() - startTime,
|
|
181
|
+
tokensUsed: totalTokens,
|
|
182
|
+
cost: totalCostUsd,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
156
186
|
return {
|
|
157
187
|
success: true,
|
|
158
188
|
output: response.content,
|
|
@@ -165,6 +195,7 @@ export class AgentRuntime {
|
|
|
165
195
|
};
|
|
166
196
|
}
|
|
167
197
|
// Execute tool calls
|
|
198
|
+
let newToolExecutions = 0;
|
|
168
199
|
for (const toolCall of response.toolCalls) {
|
|
169
200
|
const tool = allTools.find((t) => t.name === toolCall.function.name);
|
|
170
201
|
if (!tool) {
|
|
@@ -206,6 +237,7 @@ export class AgentRuntime {
|
|
|
206
237
|
? `Tool "${tool.label ?? tool.name}" denied: ${approval.reason}`
|
|
207
238
|
: `Tool "${tool.label ?? tool.name}" was denied by the user.`,
|
|
208
239
|
};
|
|
240
|
+
newToolExecutions += 1;
|
|
209
241
|
toolResults.push(denied);
|
|
210
242
|
messages.push({
|
|
211
243
|
role: 'tool',
|
|
@@ -220,8 +252,10 @@ export class AgentRuntime {
|
|
|
220
252
|
? deduplicator.getResult(tool.name, params)
|
|
221
253
|
: undefined;
|
|
222
254
|
const result = cached ?? (await tool.execute(params));
|
|
223
|
-
if (!cached)
|
|
255
|
+
if (!cached) {
|
|
224
256
|
deduplicator.record(tool.name, params, result);
|
|
257
|
+
newToolExecutions += 1;
|
|
258
|
+
}
|
|
225
259
|
toolResults.push(result);
|
|
226
260
|
// Add tool result to messages.
|
|
227
261
|
// Use result.content (LLM-optimized summary) when available;
|
|
@@ -233,12 +267,29 @@ export class AgentRuntime {
|
|
|
233
267
|
});
|
|
234
268
|
}
|
|
235
269
|
catch (error) {
|
|
270
|
+
newToolExecutions += 1;
|
|
236
271
|
toolResults.push({
|
|
237
272
|
success: false,
|
|
238
273
|
error: error instanceof Error ? error.message : String(error),
|
|
239
274
|
});
|
|
240
275
|
}
|
|
241
276
|
}
|
|
277
|
+
const stopReason = await this.tickStudioLoop(loopId, {
|
|
278
|
+
advanced: iterationAdvanced({ completed: false, newToolExecutions }),
|
|
279
|
+
...spend.tick,
|
|
280
|
+
});
|
|
281
|
+
if (stopReason) {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
error: stopReason,
|
|
285
|
+
toolResults,
|
|
286
|
+
metadata: {
|
|
287
|
+
executionTime: Date.now() - startTime,
|
|
288
|
+
tokensUsed: totalTokens,
|
|
289
|
+
cost: totalCostUsd,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
242
293
|
}
|
|
243
294
|
return {
|
|
244
295
|
success: false,
|
|
@@ -264,6 +315,42 @@ export class AgentRuntime {
|
|
|
264
315
|
};
|
|
265
316
|
}
|
|
266
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Arm LoopGuard for this task when Studio's daemon answers.
|
|
320
|
+
* Returns null when the daemon is absent or the call fails open.
|
|
321
|
+
*/
|
|
322
|
+
async openStudioLoop(taskId) {
|
|
323
|
+
if (!this.loopGuard)
|
|
324
|
+
return null;
|
|
325
|
+
const loopId = studioLoopId(taskId);
|
|
326
|
+
try {
|
|
327
|
+
const signal = await this.loopGuard.arm({ loopId });
|
|
328
|
+
return signal.attached ? loopId : null;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Report one model iteration. A non-null return is the daemon stop signal
|
|
336
|
+
* (`not_advancing` after the daemon no-op limit, default 3).
|
|
337
|
+
*/
|
|
338
|
+
async tickStudioLoop(loopId, tick) {
|
|
339
|
+
if (!(loopId && this.loopGuard))
|
|
340
|
+
return null;
|
|
341
|
+
try {
|
|
342
|
+
const signal = await this.loopGuard.tick({ loopId, ...tick });
|
|
343
|
+
if (!signal.attached)
|
|
344
|
+
return null;
|
|
345
|
+
if (signal.status === 'not_advancing') {
|
|
346
|
+
return signal.signal ?? 'loop not advancing';
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
267
354
|
/**
|
|
268
355
|
* Add task to queue
|
|
269
356
|
*/
|
|
@@ -319,3 +406,23 @@ export class AgentRuntime {
|
|
|
319
406
|
};
|
|
320
407
|
}
|
|
321
408
|
}
|
|
409
|
+
function iterationSpend(response, model, iterationTokens) {
|
|
410
|
+
const tokensIn = response.usage?.promptTokens ?? (iterationTokens > 0 ? Math.floor(iterationTokens * 0.7) : 0);
|
|
411
|
+
const tokensOut = response.usage?.completionTokens ??
|
|
412
|
+
(iterationTokens > 0 ? Math.max(0, iterationTokens - tokensIn) : 0);
|
|
413
|
+
let costUsd = 0;
|
|
414
|
+
if (model && iterationTokens > 0) {
|
|
415
|
+
costUsd =
|
|
416
|
+
estimateCost(tokensIn, model, 'input').estimatedCostUsd +
|
|
417
|
+
estimateCost(tokensOut, model, 'output').estimatedCostUsd;
|
|
418
|
+
}
|
|
419
|
+
const costMicros = costUsd > 0 ? Math.round(costUsd * 1_000_000) : 0;
|
|
420
|
+
return {
|
|
421
|
+
costUsd,
|
|
422
|
+
tick: {
|
|
423
|
+
...(tokensIn > 0 ? { tokensIn } : {}),
|
|
424
|
+
...(tokensOut > 0 ? { tokensOut } : {}),
|
|
425
|
+
...(costMicros > 0 ? { costMicros } : {}),
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streaming-runtime.d.ts","sourceRoot":"","sources":["../../src/orchestration/streaming-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"streaming-runtime.d.ts","sourceRoot":"","sources":["../../src/orchestration/streaming-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EACA,MAAM,GACN,iBAAiB,GACjB,kBAAkB,GAClB,OAAO,GACP,MAAM,GACN,cAAc,GACd,kBAAkB,GAClB,qBAAqB,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE;QACT,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE;QACT,KAAK,EAAE,MAAM,CAAC;QACd,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,uDAAuD;IACvD,WAAW,CAAC,EAAE;QACZ,aAAa,EAAE,MAAM,CAAC;QACtB,eAAe,EAAE,OAAO,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,MAAM,GAAE,aAAkB;IAItC;;;OAGG;IACI,UAAU,CACf,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,SAAS,EACpB,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,gBAAgB,CAAC;CA0MpC"}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { compressToolResult } from '../inference/tool-result-compressor.js';
|
|
11
11
|
import { toolParametersToJsonSchema } from '../llm/tool-json-schema.js';
|
|
12
12
|
import { ToolCallDeduplicator } from '../tools/deduplicator.js';
|
|
13
|
+
import { iterationAdvanced } from './loop-guard.js';
|
|
13
14
|
import { AgentRuntime } from './runtime.js';
|
|
14
15
|
export class StreamingAgentRuntime extends AgentRuntime {
|
|
15
16
|
constructor(config = {}) {
|
|
@@ -43,6 +44,7 @@ export class StreamingAgentRuntime extends AgentRuntime {
|
|
|
43
44
|
content: task.description,
|
|
44
45
|
},
|
|
45
46
|
];
|
|
47
|
+
const loopId = await this.openStudioLoop(task.id);
|
|
46
48
|
try {
|
|
47
49
|
while (iterations < maxIterations) {
|
|
48
50
|
iterations++;
|
|
@@ -96,6 +98,13 @@ export class StreamingAgentRuntime extends AgentRuntime {
|
|
|
96
98
|
}
|
|
97
99
|
// If no tool calls, task is complete
|
|
98
100
|
if (pendingToolCalls.length === 0) {
|
|
101
|
+
const stopReason = await this.tickStudioLoop(loopId, {
|
|
102
|
+
advanced: iterationAdvanced({ completed: true, newToolExecutions: 0 }),
|
|
103
|
+
});
|
|
104
|
+
if (stopReason) {
|
|
105
|
+
yield { type: 'error', error: stopReason };
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
99
108
|
yield {
|
|
100
109
|
type: 'done',
|
|
101
110
|
content: accumulatedContent,
|
|
@@ -109,7 +118,9 @@ export class StreamingAgentRuntime extends AgentRuntime {
|
|
|
109
118
|
content: accumulatedContent,
|
|
110
119
|
toolCalls: pendingToolCalls,
|
|
111
120
|
});
|
|
112
|
-
// Execute all tool calls in this response
|
|
121
|
+
// Execute all tool calls in this response.
|
|
122
|
+
// Stream chunks do not carry provider usage, so these ticks omit spend.
|
|
123
|
+
let newToolExecutions = 0;
|
|
113
124
|
for (const tc of pendingToolCalls) {
|
|
114
125
|
yield { type: 'tool_call_start', toolCall: { name: tc.name, arguments: tc.arguments } };
|
|
115
126
|
const tool = agent.tools.find((t) => t.name === tc.name);
|
|
@@ -146,6 +157,7 @@ export class StreamingAgentRuntime extends AgentRuntime {
|
|
|
146
157
|
try {
|
|
147
158
|
const result = await tool.execute(params);
|
|
148
159
|
deduplicator.record(tc.name, params, result);
|
|
160
|
+
newToolExecutions += 1;
|
|
149
161
|
toolResults.push(result);
|
|
150
162
|
yield { type: 'tool_call_result', toolResult: result };
|
|
151
163
|
messages.push({
|
|
@@ -155,6 +167,7 @@ export class StreamingAgentRuntime extends AgentRuntime {
|
|
|
155
167
|
});
|
|
156
168
|
}
|
|
157
169
|
catch (toolError) {
|
|
170
|
+
newToolExecutions += 1;
|
|
158
171
|
const result = {
|
|
159
172
|
success: false,
|
|
160
173
|
error: toolError instanceof Error ? toolError.message : String(toolError),
|
|
@@ -168,6 +181,13 @@ export class StreamingAgentRuntime extends AgentRuntime {
|
|
|
168
181
|
});
|
|
169
182
|
}
|
|
170
183
|
}
|
|
184
|
+
const stopReason = await this.tickStudioLoop(loopId, {
|
|
185
|
+
advanced: iterationAdvanced({ completed: false, newToolExecutions }),
|
|
186
|
+
});
|
|
187
|
+
if (stopReason) {
|
|
188
|
+
yield { type: 'error', error: stopReason };
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
171
191
|
}
|
|
172
192
|
yield { type: 'error', error: 'Maximum iterations reached' };
|
|
173
193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "AI runtime for agent-driven products — agents, memory, LLM providers (Inference Snaps, Ollama, OpenAI-compatible), tools, and orchestration. Anthropic-SDK-free.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"lru-cache": "^11.5.1",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
|
-
"@revealui/contracts": "0.
|
|
29
|
-
"@revealui/core": "0.15.
|
|
30
|
-
"@revealui/db": "0.
|
|
28
|
+
"@revealui/contracts": "0.13.2",
|
|
29
|
+
"@revealui/core": "0.15.3",
|
|
30
|
+
"@revealui/db": "0.12.1",
|
|
31
31
|
"@revealui/resilience": "0.2.4"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|