@revealui/ai 1.0.7 → 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 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,53 @@
1
+ /**
2
+ * TypeSafe Jev — System One decision adapter.
3
+ *
4
+ * Not a chat model. Send state plus typed questions. The response is
5
+ * choices, scores, and noul probabilities. No TypeSafe SDK: one HTTPS POST.
6
+ * The key is the caller's TYPESAFE_API_KEY. RevealUI does not host one.
7
+ *
8
+ * Docs: https://docs.typesafe.ai/introduction/quickstart
9
+ */
10
+ export declare const JEV_MODEL = "jev-latest";
11
+ export declare const TYPESAFE_SYSTEMONE_URL = "https://api.typesafe.ai/v1/systemone";
12
+ export interface JevChoiceQuestion {
13
+ readonly type: 'choice';
14
+ readonly instructions: string;
15
+ readonly criteria: Readonly<Record<string, string>>;
16
+ }
17
+ export interface JevScoreQuestion {
18
+ readonly type: 'score';
19
+ readonly instructions: string;
20
+ readonly criteria: readonly string[];
21
+ }
22
+ export interface JevNoulQuestion {
23
+ readonly type: 'noul';
24
+ readonly instructions: string;
25
+ }
26
+ export type JevQuestion = JevChoiceQuestion | JevScoreQuestion | JevNoulQuestion;
27
+ export interface JevEvaluateInput {
28
+ readonly state: string | Record<string, unknown> | readonly unknown[];
29
+ readonly questions: Readonly<Record<string, JevQuestion>>;
30
+ readonly model?: string;
31
+ }
32
+ export interface JevUsage {
33
+ readonly input_tokens: number;
34
+ readonly output_tokens: number;
35
+ }
36
+ export interface JevEvaluateResult {
37
+ readonly model: string;
38
+ readonly answers: Readonly<Record<string, unknown>>;
39
+ readonly usage?: JevUsage;
40
+ }
41
+ export declare class JevConfigError extends Error {
42
+ constructor(message: string);
43
+ }
44
+ export declare function typesafeApiKey(env?: NodeJS.ProcessEnv): string;
45
+ /**
46
+ * One System One call. Does not generate text and is not the local snap default.
47
+ */
48
+ export declare function evaluateJev(input: JevEvaluateInput, options?: {
49
+ readonly fetchImpl?: typeof fetch;
50
+ readonly apiKey?: string;
51
+ readonly url?: string;
52
+ }): Promise<JevEvaluateResult>;
53
+ //# sourceMappingURL=typesafe-jev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typesafe-jev.d.ts","sourceRoot":"","sources":["../../../src/llm/providers/typesafe-jev.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,SAAS,eAAe,CAAC;AACtC,eAAO,MAAM,sBAAsB,yCAAyC,CAAC;AAE7E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,CAAC;AAEjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,OAAO,EAAE,CAAC;IACtE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAM3E;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/F,OAAO,CAAC,iBAAiB,CAAC,CAsB5B"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TypeSafe Jev — System One decision adapter.
3
+ *
4
+ * Not a chat model. Send state plus typed questions. The response is
5
+ * choices, scores, and noul probabilities. No TypeSafe SDK: one HTTPS POST.
6
+ * The key is the caller's TYPESAFE_API_KEY. RevealUI does not host one.
7
+ *
8
+ * Docs: https://docs.typesafe.ai/introduction/quickstart
9
+ */
10
+ export const JEV_MODEL = 'jev-latest';
11
+ export const TYPESAFE_SYSTEMONE_URL = 'https://api.typesafe.ai/v1/systemone';
12
+ export class JevConfigError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'JevConfigError';
16
+ }
17
+ }
18
+ export function typesafeApiKey(env = process.env) {
19
+ const key = env.TYPESAFE_API_KEY?.trim();
20
+ if (!key) {
21
+ throw new JevConfigError('TYPESAFE_API_KEY is required for Jev. RevealUI does not ship one.');
22
+ }
23
+ return key;
24
+ }
25
+ /**
26
+ * One System One call. Does not generate text and is not the local snap default.
27
+ */
28
+ export async function evaluateJev(input, options) {
29
+ const fetchImpl = options?.fetchImpl ?? fetch;
30
+ const response = await fetchImpl(options?.url ?? TYPESAFE_SYSTEMONE_URL, {
31
+ method: 'POST',
32
+ headers: {
33
+ Authorization: `Bearer ${options?.apiKey ?? typesafeApiKey()}`,
34
+ 'Content-Type': 'application/json',
35
+ },
36
+ body: JSON.stringify({
37
+ state: input.state,
38
+ model: input.model ?? JEV_MODEL,
39
+ questions: input.questions,
40
+ }),
41
+ });
42
+ if (!response.ok) {
43
+ throw new Error(`Jev request failed: HTTP ${response.status}`);
44
+ }
45
+ const body = (await response.json());
46
+ if (!body || typeof body !== 'object' || !body.answers) {
47
+ throw new Error('Jev response missing answers');
48
+ }
49
+ return body;
50
+ }
@@ -10,6 +10,7 @@ export * from './providers/groq.js';
10
10
  export * from './providers/inference-snaps.js';
11
11
  export * from './providers/ollama.js';
12
12
  export * from './providers/openai-compat.js';
13
+ export * from './providers/typesafe-jev.js';
13
14
  export * from './resolve.js';
14
15
  export * from './workspace-provider-config.js';
15
16
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/llm/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,aAAa,CAAC;AAE5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,uBAAuB,CAAC;AACtC,cAAc,8BAA8B,CAAC;AAE7C,cAAc,cAAc,CAAC;AAI7B,cAAc,gCAAgC,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/llm/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,aAAa,CAAC;AAE5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,uBAAuB,CAAC;AACtC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAE5C,cAAc,cAAc,CAAC;AAI7B,cAAc,gCAAgC,CAAC"}
@@ -12,6 +12,7 @@ export * from './providers/groq.js';
12
12
  export * from './providers/inference-snaps.js';
13
13
  export * from './providers/ollama.js';
14
14
  export * from './providers/openai-compat.js';
15
+ export * from './providers/typesafe-jev.js';
15
16
  // Export the per-request client resolver (GAP-360 PR-2)
16
17
  export * from './resolve.js';
17
18
  // Export per-workspace provider registry (used by admin inference-config route
@@ -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
- * Executes agent tasks with tool execution, memory management, and error handling
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;;;;GAIG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAW,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAGzE,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;AAE3D;;;;;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;CAClC;AAED,qBAAa,YAAY;IACvB,SAAS,CAAC,MAAM,EAAE,aAAa,CAAC;IAChC,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,cAAc,CAAgD;IACtE,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,GAAE,aAAkB;IA0BtC;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;YAqBzE,OAAO;IA6OrB;;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
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/orchestration/runtime.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,18 @@
1
1
  /**
2
2
  * Agent Runtime
3
3
  *
4
- * Executes agent tasks with tool execution, memory management, and error handling
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
- if (model && iterationTokens > 0) {
142
- const inputTokens = response.usage?.promptTokens ?? Math.floor(iterationTokens * 0.7);
143
- const outputTokens = response.usage?.completionTokens ?? iterationTokens - inputTokens;
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;AAC9C,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;CAsLpC"}
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.0.7",
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.11.0",
29
- "@revealui/core": "0.14.7",
30
- "@revealui/db": "0.10.3",
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": {