@beeos-ai/device-agent 0.5.0 → 0.6.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/.pi/extensions/android-tools.ts +24 -3
- package/dist/adapters/identity-file.d.ts +33 -5
- package/dist/adapters/identity-file.js +52 -6
- package/dist/adapters/identity-file.js.map +1 -1
- package/dist/app/in-flight-turn.d.ts +120 -0
- package/dist/app/in-flight-turn.js +158 -0
- package/dist/app/in-flight-turn.js.map +1 -0
- package/dist/app/jsonl-store.d.ts +109 -0
- package/dist/app/jsonl-store.js +402 -0
- package/dist/app/jsonl-store.js.map +1 -0
- package/dist/app/message-store.d.ts +82 -31
- package/dist/app/message-store.js +109 -48
- package/dist/app/message-store.js.map +1 -1
- package/dist/app/session-manager.d.ts +120 -57
- package/dist/app/session-manager.js +319 -123
- package/dist/app/session-manager.js.map +1 -1
- package/dist/config/paths.d.ts +48 -0
- package/dist/config/paths.js +59 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/fleet/adapters/process/node-process-manager.js +97 -17
- package/dist/fleet/adapters/process/node-process-manager.js.map +1 -1
- package/dist/fleet/adapters/state/beeos-cli-store.d.ts +75 -9
- package/dist/fleet/adapters/state/beeos-cli-store.js +201 -17
- package/dist/fleet/adapters/state/beeos-cli-store.js.map +1 -1
- package/dist/fleet/domain/device.d.ts +9 -0
- package/dist/fleet/domain/device.js.map +1 -1
- package/dist/fleet/domain/fsm.js +34 -0
- package/dist/fleet/domain/fsm.js.map +1 -1
- package/dist/fleet/installer/spawn-env.d.ts +3 -0
- package/dist/fleet/installer/spawn-env.js +186 -3
- package/dist/fleet/installer/spawn-env.js.map +1 -1
- package/dist/fleet/state/state-json.d.ts +116 -0
- package/dist/fleet/state/state-json.js +87 -0
- package/dist/fleet/state/state-json.js.map +1 -0
- package/dist/fleet/supervisor.js +14 -2
- package/dist/fleet/supervisor.js.map +1 -1
- package/dist/inbound/acp/handlers/initialize.d.ts +8 -0
- package/dist/inbound/acp/handlers/initialize.js +9 -1
- package/dist/inbound/acp/handlers/initialize.js.map +1 -1
- package/dist/inbound/acp/handlers/session.d.ts +0 -1
- package/dist/orchestrator-system-prompt.js +2 -2
- package/dist/plugins/acp/index.d.ts +18 -1
- package/dist/plugins/acp/index.js +49 -3
- package/dist/plugins/acp/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -47,6 +47,18 @@ import { Type } from "@sinclair/typebox";
|
|
|
47
47
|
import { getPiExtensionContext } from "../../src/pi-context.js";
|
|
48
48
|
import type { Shortcut, ShortcutResult } from "../../src/shortcuts/index.js";
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Server-side floor for `mobile_use` step budget.
|
|
52
|
+
*
|
|
53
|
+
* The outer orchestrator LLM frequently misjudges ADB / Android tasks as
|
|
54
|
+
* "simple actions" and emits `max_steps: 5..15`, which the inner
|
|
55
|
+
* `Agent.run` loop blows through before the task can finish — surfacing as
|
|
56
|
+
* "Reached max steps (N)". Clamping upward at the tool boundary protects
|
|
57
|
+
* the user without preventing the LLM from explicitly asking for more
|
|
58
|
+
* steps when it judges a flow as complex.
|
|
59
|
+
*/
|
|
60
|
+
const MIN_MOBILE_USE_STEPS = 30;
|
|
61
|
+
|
|
50
62
|
export default function (pi: ExtensionAPI) {
|
|
51
63
|
// ── mobile_use ─────────────────────────────────────────────────────────────
|
|
52
64
|
pi.registerTool({
|
|
@@ -57,12 +69,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
57
69
|
"A VLM runs its own screenshot→reason→act loop until it finishes or hits max-steps. " +
|
|
58
70
|
"Pass a full, concrete instruction with app-native UI text. " +
|
|
59
71
|
"For long text (especially Chinese), embed it verbatim and note '原样输入、不要翻译'. " +
|
|
60
|
-
"One call runs
|
|
72
|
+
"One call runs 20–50 steps autonomously; do NOT call repeatedly for the same task.",
|
|
61
73
|
promptSnippet: "mobile_use(task): drive Android phone via VLM screenshot→reason→act loop",
|
|
62
74
|
parameters: Type.Object({
|
|
63
75
|
task: Type.String({ description: "Full natural-language instruction in Chinese or English" }),
|
|
64
76
|
max_steps: Type.Integer({
|
|
65
|
-
description: "Maximum VLM steps for this call. Required. Typical range
|
|
77
|
+
description: "Maximum VLM steps for this call. Required. Typical range 20-50 depending on task complexity.",
|
|
66
78
|
minimum: 1,
|
|
67
79
|
maximum: 100,
|
|
68
80
|
}),
|
|
@@ -72,7 +84,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
72
84
|
if (signal?.aborted) throw new Error("aborted");
|
|
73
85
|
|
|
74
86
|
const ctx = getPiExtensionContext();
|
|
75
|
-
const
|
|
87
|
+
const requested = params.max_steps;
|
|
88
|
+
const effective = Math.max(MIN_MOBILE_USE_STEPS, requested);
|
|
89
|
+
if (effective !== requested) {
|
|
90
|
+
const log = (await import("../../src/logger/index.js")).getLogger();
|
|
91
|
+
log.warn(
|
|
92
|
+
{ component: "mobile_use", requested, effective, floor: MIN_MOBILE_USE_STEPS },
|
|
93
|
+
"mobile_use.max_steps clamped up to floor",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const result = await ctx.runMobileUse(params.task, effective, signal ?? undefined);
|
|
76
97
|
return { content: [{ type: "text", text: result }], details: null };
|
|
77
98
|
},
|
|
78
99
|
});
|
|
@@ -4,18 +4,36 @@
|
|
|
4
4
|
* On-disk format follows the SDK convention used by `@beeos-ai/bridge-client`:
|
|
5
5
|
* { "publicKey": "<base64>", "privateKey": "<base64>" }
|
|
6
6
|
*
|
|
7
|
-
* Default location
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Default location resolution (in order):
|
|
8
|
+
* 1. Explicit `keyFile` (passed by the CLI / `--key` flag).
|
|
9
|
+
* 2. The modern CLI-aligned layout
|
|
10
|
+
* `~/.beeos/identity/device-<id|"default">.key.json` — this matches
|
|
11
|
+
* `web/packages/core/src/runtime/device.ts::deviceKeyPath` so a
|
|
12
|
+
* manually-launched device-agent reads the SAME key the CLI's
|
|
13
|
+
* `beeos device attach` would write.
|
|
14
|
+
* 3. The legacy flat path `~/.beeos/<id|"device-agent">.key` — kept
|
|
15
|
+
* for back-compat with v0.4.x installs but deprecated.
|
|
16
|
+
* A `console.warn` fires the first time we resolve via this
|
|
17
|
+
* branch so users are nudged to migrate.
|
|
18
|
+
*
|
|
19
|
+
* Migration plan:
|
|
20
|
+
* - 0.5.x (current): both paths resolve, legacy emits a warn.
|
|
21
|
+
* - 0.6.x: legacy path stops resolving; users must move the file to
|
|
22
|
+
* the new location or pass `--key` explicitly.
|
|
10
23
|
*/
|
|
11
24
|
import type { IDeviceIdentity, KeyPair } from "../ports/identity.js";
|
|
12
25
|
export interface FileIdentityOptions {
|
|
13
26
|
/**
|
|
14
27
|
* Absolute path to the key file.
|
|
15
|
-
* If undefined,
|
|
28
|
+
* If undefined, the resolver picks the modern (CLI-aligned) layout
|
|
29
|
+
* with a fallback to the legacy flat path — see file header.
|
|
16
30
|
*/
|
|
17
31
|
keyFile?: string;
|
|
18
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Suffix used when computing the default file name. For per-device
|
|
34
|
+
* agent processes the CLI passes the ADB serial here so each device
|
|
35
|
+
* gets its own file.
|
|
36
|
+
*/
|
|
19
37
|
id?: string;
|
|
20
38
|
}
|
|
21
39
|
export declare class FileIdentity implements IDeviceIdentity {
|
|
@@ -28,3 +46,13 @@ export declare class FileIdentity implements IDeviceIdentity {
|
|
|
28
46
|
publicKeyBase64(): Promise<string>;
|
|
29
47
|
fingerprint(): Promise<string>;
|
|
30
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the implicit key-file path when no explicit `keyFile` was
|
|
51
|
+
* passed. Honours backwards compatibility: if the legacy
|
|
52
|
+
* `~/.beeos/<id>.key` already exists on disk we keep using it (so we
|
|
53
|
+
* don't strand existing v0.4.x installs without a key) but emit a
|
|
54
|
+
* one-time deprecation warn pointing at the new location.
|
|
55
|
+
*
|
|
56
|
+
* Exported for unit tests — not part of the runtime contract.
|
|
57
|
+
*/
|
|
58
|
+
export declare function defaultKeyFilePath(id?: string): string;
|
|
@@ -4,21 +4,33 @@
|
|
|
4
4
|
* On-disk format follows the SDK convention used by `@beeos-ai/bridge-client`:
|
|
5
5
|
* { "publicKey": "<base64>", "privateKey": "<base64>" }
|
|
6
6
|
*
|
|
7
|
-
* Default location
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Default location resolution (in order):
|
|
8
|
+
* 1. Explicit `keyFile` (passed by the CLI / `--key` flag).
|
|
9
|
+
* 2. The modern CLI-aligned layout
|
|
10
|
+
* `~/.beeos/identity/device-<id|"default">.key.json` — this matches
|
|
11
|
+
* `web/packages/core/src/runtime/device.ts::deviceKeyPath` so a
|
|
12
|
+
* manually-launched device-agent reads the SAME key the CLI's
|
|
13
|
+
* `beeos device attach` would write.
|
|
14
|
+
* 3. The legacy flat path `~/.beeos/<id|"device-agent">.key` — kept
|
|
15
|
+
* for back-compat with v0.4.x installs but deprecated.
|
|
16
|
+
* A `console.warn` fires the first time we resolve via this
|
|
17
|
+
* branch so users are nudged to migrate.
|
|
18
|
+
*
|
|
19
|
+
* Migration plan:
|
|
20
|
+
* - 0.5.x (current): both paths resolve, legacy emits a warn.
|
|
21
|
+
* - 0.6.x: legacy path stops resolving; users must move the file to
|
|
22
|
+
* the new location or pass `--key` explicitly.
|
|
10
23
|
*/
|
|
11
24
|
import * as fs from "node:fs";
|
|
12
25
|
import * as os from "node:os";
|
|
13
26
|
import * as path from "node:path";
|
|
14
27
|
import { fingerprint, loadKeyPair, publicKeyFromPrivate, } from "@beeos-ai/bridge-client";
|
|
28
|
+
let legacyKeyWarnLogged = false;
|
|
15
29
|
export class FileIdentity {
|
|
16
30
|
keyFile;
|
|
17
31
|
cached = null;
|
|
18
32
|
constructor(opts = {}) {
|
|
19
|
-
this.keyFile =
|
|
20
|
-
opts.keyFile ??
|
|
21
|
-
path.join(os.homedir(), ".beeos", `${opts.id ?? "device-agent"}.key`);
|
|
33
|
+
this.keyFile = opts.keyFile ?? defaultKeyFilePath(opts.id);
|
|
22
34
|
}
|
|
23
35
|
/** Path of the underlying file (useful for diagnostics). */
|
|
24
36
|
get path() {
|
|
@@ -47,4 +59,38 @@ export class FileIdentity {
|
|
|
47
59
|
return fingerprint(kp.publicKey);
|
|
48
60
|
}
|
|
49
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the implicit key-file path when no explicit `keyFile` was
|
|
64
|
+
* passed. Honours backwards compatibility: if the legacy
|
|
65
|
+
* `~/.beeos/<id>.key` already exists on disk we keep using it (so we
|
|
66
|
+
* don't strand existing v0.4.x installs without a key) but emit a
|
|
67
|
+
* one-time deprecation warn pointing at the new location.
|
|
68
|
+
*
|
|
69
|
+
* Exported for unit tests — not part of the runtime contract.
|
|
70
|
+
*/
|
|
71
|
+
export function defaultKeyFilePath(id) {
|
|
72
|
+
const home = os.homedir();
|
|
73
|
+
const suffix = id ?? "default";
|
|
74
|
+
const modernDir = path.join(home, ".beeos", "identity");
|
|
75
|
+
const modern = path.join(modernDir, `device-${suffix}.key.json`);
|
|
76
|
+
// Modern path is the desired target; if it exists, prefer it.
|
|
77
|
+
if (fs.existsSync(modern))
|
|
78
|
+
return modern;
|
|
79
|
+
// Legacy fallback — only matches if the user has a prior install.
|
|
80
|
+
// We accept either "<id>.key" or "device-agent.key" depending on
|
|
81
|
+
// whether `id` was passed at all (the historical default).
|
|
82
|
+
const legacyName = id ? `${id}.key` : "device-agent.key";
|
|
83
|
+
const legacy = path.join(home, ".beeos", legacyName);
|
|
84
|
+
if (fs.existsSync(legacy)) {
|
|
85
|
+
if (!legacyKeyWarnLogged) {
|
|
86
|
+
legacyKeyWarnLogged = true;
|
|
87
|
+
const next = path.join(modernDir, `device-${suffix}.key.json`);
|
|
88
|
+
console.warn(`[device-agent] using legacy identity path ${legacy} — please migrate to ${next} ` +
|
|
89
|
+
"before the v0.6 release. See `beeos device attach --help` for the canonical layout.");
|
|
90
|
+
}
|
|
91
|
+
return legacy;
|
|
92
|
+
}
|
|
93
|
+
// No file exists yet — create the new one.
|
|
94
|
+
return modern;
|
|
95
|
+
}
|
|
50
96
|
//# sourceMappingURL=identity-file.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-file.js","sourceRoot":"","sources":["../../src/adapters/identity-file.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"identity-file.js","sourceRoot":"","sources":["../../src/adapters/identity-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight assistant turn aggregator.
|
|
3
|
+
*
|
|
4
|
+
* A single ACP `session/prompt` produces a stream of host events
|
|
5
|
+
* (`task:text` deltas, `task:tool` start/update/end, etc.). Persisting
|
|
6
|
+
* each delta as a separate row produces unusable transcripts on reload
|
|
7
|
+
* — the resulting bubble is fragmented into dozens of tiny rows with
|
|
8
|
+
* no clean way to reconstruct what the agent actually said. To match
|
|
9
|
+
* OpenClaw's "one JSONL line = one complete assistant turn" model, we
|
|
10
|
+
* accumulate the delta stream in memory here and only commit a single
|
|
11
|
+
* `AssistantTurnRecord` when the turn terminates.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
* - `start(...)` — called from `task:start`, allocates a buffer.
|
|
15
|
+
* - `appendText(delta)` / `noteToolStart` / `noteToolUpdate` /
|
|
16
|
+
* `noteToolEnd` — accumulate during the turn.
|
|
17
|
+
* - `noteFatalNotice(text)` — capture terminal error text so the
|
|
18
|
+
* persisted turn carries an `error` field even if the agent's
|
|
19
|
+
* final reply is empty.
|
|
20
|
+
* - `finalize(stopReason)` — return an immutable
|
|
21
|
+
* `AssistantTurnRecord` ready to be appended to JSONL and emitted
|
|
22
|
+
* to the client. Caller resets the buffer afterwards.
|
|
23
|
+
*
|
|
24
|
+
* This module is pure data + state machine — no IO, no notifications.
|
|
25
|
+
* SessionManager owns the IO + dispatch side.
|
|
26
|
+
*/
|
|
27
|
+
/** Final status of a single tool invocation inside one turn. */
|
|
28
|
+
export type ToolCallStatus = "in_progress" | "completed" | "failed";
|
|
29
|
+
/** Stop reason mirrored from beeos-claw's three-valued enum. */
|
|
30
|
+
export type StopReason = "end_turn" | "cancelled" | "error";
|
|
31
|
+
export interface InFlightToolCall {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
args: unknown;
|
|
35
|
+
status: ToolCallStatus;
|
|
36
|
+
/** Latest output observed (overwritten on each `update` and `end`). */
|
|
37
|
+
output?: string;
|
|
38
|
+
startedAt: number;
|
|
39
|
+
endedAt?: number;
|
|
40
|
+
}
|
|
41
|
+
/** Persisted shape — what gets written to JSONL on `task:end`. */
|
|
42
|
+
export interface PersistedToolCall {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
args: unknown;
|
|
46
|
+
status: ToolCallStatus;
|
|
47
|
+
output?: string;
|
|
48
|
+
startedAt: number;
|
|
49
|
+
endedAt?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface AssistantTurnRecord {
|
|
52
|
+
/** Stable `messageId` stamped on every chunk during this turn. */
|
|
53
|
+
messageId: string;
|
|
54
|
+
/** Stable `_turnId` stamped on every chunk during this turn. */
|
|
55
|
+
turnId: string;
|
|
56
|
+
/** Wall-clock time when the turn started (epoch ms). */
|
|
57
|
+
startedAt: number;
|
|
58
|
+
/** Wall-clock time when the turn finalized (epoch ms). */
|
|
59
|
+
completedAt: number;
|
|
60
|
+
/** Concatenated assistant reply (sum of every `task:text` delta). */
|
|
61
|
+
text: string;
|
|
62
|
+
/** Tool calls in invocation order (insertion order is preserved). */
|
|
63
|
+
toolCalls: PersistedToolCall[];
|
|
64
|
+
/** Stop reason at finalize time. */
|
|
65
|
+
stopReason: StopReason;
|
|
66
|
+
/** User-facing error text when the turn terminated abnormally. */
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Mutable in-flight buffer. Construct once per turn via
|
|
71
|
+
* `InFlightTurnAggregator.start(...)`, accumulate, then `finalize()`.
|
|
72
|
+
*/
|
|
73
|
+
export declare class InFlightTurnAggregator {
|
|
74
|
+
readonly taskId: string;
|
|
75
|
+
readonly turnId: string;
|
|
76
|
+
readonly messageId: string;
|
|
77
|
+
readonly startedAt: number;
|
|
78
|
+
private text;
|
|
79
|
+
private readonly toolCalls;
|
|
80
|
+
/** Insertion order tracker — `Map` already preserves it but we keep
|
|
81
|
+
* an explicit list for tests / iteration clarity. */
|
|
82
|
+
private readonly toolOrder;
|
|
83
|
+
private errorText;
|
|
84
|
+
constructor(taskId: string, turnId: string, messageId: string, startedAt?: number);
|
|
85
|
+
/** Convenience constructor mirroring SessionManager call sites. */
|
|
86
|
+
static start(opts: {
|
|
87
|
+
taskId: string;
|
|
88
|
+
turnId: string;
|
|
89
|
+
messageId: string;
|
|
90
|
+
now?: number;
|
|
91
|
+
}): InFlightTurnAggregator;
|
|
92
|
+
/** Append a text delta to the in-flight reply. Empty deltas are no-ops. */
|
|
93
|
+
appendText(delta: string): void;
|
|
94
|
+
/** Read-only snapshot of the accumulated text — used by load-replay. */
|
|
95
|
+
currentText(): string;
|
|
96
|
+
/** Read-only ordered list of tool calls — used by load-replay. */
|
|
97
|
+
currentToolCalls(): readonly InFlightToolCall[];
|
|
98
|
+
noteToolStart(opts: {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
args: unknown;
|
|
102
|
+
now?: number;
|
|
103
|
+
}): void;
|
|
104
|
+
noteToolUpdate(opts: {
|
|
105
|
+
id: string;
|
|
106
|
+
output?: string;
|
|
107
|
+
}): void;
|
|
108
|
+
noteToolEnd(opts: {
|
|
109
|
+
id: string;
|
|
110
|
+
output?: string;
|
|
111
|
+
isError?: boolean;
|
|
112
|
+
now?: number;
|
|
113
|
+
}): void;
|
|
114
|
+
/** Capture terminal error text from a `task:notice level=error`. */
|
|
115
|
+
noteFatalNotice(text: string): void;
|
|
116
|
+
/** Whether anything would actually be persisted on finalize. */
|
|
117
|
+
isEmpty(): boolean;
|
|
118
|
+
/** Produce the immutable record ready for JSONL append + replay. */
|
|
119
|
+
finalize(stopReason: StopReason, now?: number): AssistantTurnRecord;
|
|
120
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight assistant turn aggregator.
|
|
3
|
+
*
|
|
4
|
+
* A single ACP `session/prompt` produces a stream of host events
|
|
5
|
+
* (`task:text` deltas, `task:tool` start/update/end, etc.). Persisting
|
|
6
|
+
* each delta as a separate row produces unusable transcripts on reload
|
|
7
|
+
* — the resulting bubble is fragmented into dozens of tiny rows with
|
|
8
|
+
* no clean way to reconstruct what the agent actually said. To match
|
|
9
|
+
* OpenClaw's "one JSONL line = one complete assistant turn" model, we
|
|
10
|
+
* accumulate the delta stream in memory here and only commit a single
|
|
11
|
+
* `AssistantTurnRecord` when the turn terminates.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
* - `start(...)` — called from `task:start`, allocates a buffer.
|
|
15
|
+
* - `appendText(delta)` / `noteToolStart` / `noteToolUpdate` /
|
|
16
|
+
* `noteToolEnd` — accumulate during the turn.
|
|
17
|
+
* - `noteFatalNotice(text)` — capture terminal error text so the
|
|
18
|
+
* persisted turn carries an `error` field even if the agent's
|
|
19
|
+
* final reply is empty.
|
|
20
|
+
* - `finalize(stopReason)` — return an immutable
|
|
21
|
+
* `AssistantTurnRecord` ready to be appended to JSONL and emitted
|
|
22
|
+
* to the client. Caller resets the buffer afterwards.
|
|
23
|
+
*
|
|
24
|
+
* This module is pure data + state machine — no IO, no notifications.
|
|
25
|
+
* SessionManager owns the IO + dispatch side.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Mutable in-flight buffer. Construct once per turn via
|
|
29
|
+
* `InFlightTurnAggregator.start(...)`, accumulate, then `finalize()`.
|
|
30
|
+
*/
|
|
31
|
+
export class InFlightTurnAggregator {
|
|
32
|
+
taskId;
|
|
33
|
+
turnId;
|
|
34
|
+
messageId;
|
|
35
|
+
startedAt;
|
|
36
|
+
text = "";
|
|
37
|
+
toolCalls = new Map();
|
|
38
|
+
/** Insertion order tracker — `Map` already preserves it but we keep
|
|
39
|
+
* an explicit list for tests / iteration clarity. */
|
|
40
|
+
toolOrder = [];
|
|
41
|
+
errorText;
|
|
42
|
+
constructor(taskId, turnId, messageId, startedAt = Date.now()) {
|
|
43
|
+
this.taskId = taskId;
|
|
44
|
+
this.turnId = turnId;
|
|
45
|
+
this.messageId = messageId;
|
|
46
|
+
this.startedAt = startedAt;
|
|
47
|
+
}
|
|
48
|
+
/** Convenience constructor mirroring SessionManager call sites. */
|
|
49
|
+
static start(opts) {
|
|
50
|
+
return new InFlightTurnAggregator(opts.taskId, opts.turnId, opts.messageId, opts.now ?? Date.now());
|
|
51
|
+
}
|
|
52
|
+
/** Append a text delta to the in-flight reply. Empty deltas are no-ops. */
|
|
53
|
+
appendText(delta) {
|
|
54
|
+
if (!delta)
|
|
55
|
+
return;
|
|
56
|
+
this.text += delta;
|
|
57
|
+
}
|
|
58
|
+
/** Read-only snapshot of the accumulated text — used by load-replay. */
|
|
59
|
+
currentText() {
|
|
60
|
+
return this.text;
|
|
61
|
+
}
|
|
62
|
+
/** Read-only ordered list of tool calls — used by load-replay. */
|
|
63
|
+
currentToolCalls() {
|
|
64
|
+
return this.toolOrder.map((id) => this.toolCalls.get(id)).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
noteToolStart(opts) {
|
|
67
|
+
if (this.toolCalls.has(opts.id)) {
|
|
68
|
+
// Re-issued start (rare — agent core sometimes replays). Reset
|
|
69
|
+
// to the new args/name but preserve insertion order.
|
|
70
|
+
this.toolCalls.set(opts.id, {
|
|
71
|
+
id: opts.id,
|
|
72
|
+
name: opts.name,
|
|
73
|
+
args: opts.args,
|
|
74
|
+
status: "in_progress",
|
|
75
|
+
startedAt: opts.now ?? Date.now(),
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.toolCalls.set(opts.id, {
|
|
80
|
+
id: opts.id,
|
|
81
|
+
name: opts.name,
|
|
82
|
+
args: opts.args,
|
|
83
|
+
status: "in_progress",
|
|
84
|
+
startedAt: opts.now ?? Date.now(),
|
|
85
|
+
});
|
|
86
|
+
this.toolOrder.push(opts.id);
|
|
87
|
+
}
|
|
88
|
+
noteToolUpdate(opts) {
|
|
89
|
+
const existing = this.toolCalls.get(opts.id);
|
|
90
|
+
if (!existing)
|
|
91
|
+
return;
|
|
92
|
+
if (opts.output !== undefined)
|
|
93
|
+
existing.output = opts.output;
|
|
94
|
+
// status stays "in_progress" — terminal status comes via noteToolEnd.
|
|
95
|
+
}
|
|
96
|
+
noteToolEnd(opts) {
|
|
97
|
+
const existing = this.toolCalls.get(opts.id);
|
|
98
|
+
if (!existing) {
|
|
99
|
+
// End-without-start (some adapters skip the start hook). Synthesise a
|
|
100
|
+
// record so we don't lose the terminal state.
|
|
101
|
+
this.toolCalls.set(opts.id, {
|
|
102
|
+
id: opts.id,
|
|
103
|
+
name: opts.id,
|
|
104
|
+
args: {},
|
|
105
|
+
status: opts.isError ? "failed" : "completed",
|
|
106
|
+
output: opts.output,
|
|
107
|
+
startedAt: opts.now ?? Date.now(),
|
|
108
|
+
endedAt: opts.now ?? Date.now(),
|
|
109
|
+
});
|
|
110
|
+
this.toolOrder.push(opts.id);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
existing.status = opts.isError ? "failed" : "completed";
|
|
114
|
+
if (opts.output !== undefined)
|
|
115
|
+
existing.output = opts.output;
|
|
116
|
+
existing.endedAt = opts.now ?? Date.now();
|
|
117
|
+
}
|
|
118
|
+
/** Capture terminal error text from a `task:notice level=error`. */
|
|
119
|
+
noteFatalNotice(text) {
|
|
120
|
+
if (text)
|
|
121
|
+
this.errorText = text;
|
|
122
|
+
}
|
|
123
|
+
/** Whether anything would actually be persisted on finalize. */
|
|
124
|
+
isEmpty() {
|
|
125
|
+
return this.text.length === 0 && this.toolOrder.length === 0 && !this.errorText;
|
|
126
|
+
}
|
|
127
|
+
/** Produce the immutable record ready for JSONL append + replay. */
|
|
128
|
+
finalize(stopReason, now = Date.now()) {
|
|
129
|
+
const toolCalls = this.toolOrder.map((id) => {
|
|
130
|
+
const tc = this.toolCalls.get(id);
|
|
131
|
+
const out = {
|
|
132
|
+
id: tc.id,
|
|
133
|
+
name: tc.name,
|
|
134
|
+
args: tc.args,
|
|
135
|
+
status: tc.status,
|
|
136
|
+
startedAt: tc.startedAt,
|
|
137
|
+
};
|
|
138
|
+
if (tc.output !== undefined)
|
|
139
|
+
out.output = tc.output;
|
|
140
|
+
if (tc.endedAt !== undefined)
|
|
141
|
+
out.endedAt = tc.endedAt;
|
|
142
|
+
return out;
|
|
143
|
+
});
|
|
144
|
+
const record = {
|
|
145
|
+
messageId: this.messageId,
|
|
146
|
+
turnId: this.turnId,
|
|
147
|
+
startedAt: this.startedAt,
|
|
148
|
+
completedAt: now,
|
|
149
|
+
text: this.text,
|
|
150
|
+
toolCalls,
|
|
151
|
+
stopReason,
|
|
152
|
+
};
|
|
153
|
+
if (this.errorText)
|
|
154
|
+
record.error = this.errorText;
|
|
155
|
+
return record;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=in-flight-turn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-flight-turn.js","sourceRoot":"","sources":["../../src/app/in-flight-turn.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only JSONL persistence layer for `MessageStore`.
|
|
3
|
+
*
|
|
4
|
+
* Layout (one file per session, plus one metadata file):
|
|
5
|
+
*
|
|
6
|
+
* ${BEEOS_HOME ?? ~/.beeos}/device-agent/sessions/
|
|
7
|
+
* ├── <sid>.jsonl # append-only record log
|
|
8
|
+
* └── <sid>.meta.json # { sessionId, title, createdAt, updatedAt }
|
|
9
|
+
*
|
|
10
|
+
* Each JSONL line is a serialised `PersistedRecord` (see message-store.ts):
|
|
11
|
+
*
|
|
12
|
+
* {"v":1,"type":"user_message","id":"...","ts":...,"content":"...","turnId":"...","messageId":"..."}
|
|
13
|
+
* {"v":1,"type":"assistant_turn","id":"...","ts":...,"messageId":"...","turnId":"...",
|
|
14
|
+
* "startedAt":...,"completedAt":...,"text":"...","toolCalls":[...],"stopReason":"...","error":"..."}
|
|
15
|
+
*
|
|
16
|
+
* IO strategy:
|
|
17
|
+
*
|
|
18
|
+
* - Appends use `fs.writeSync` against a long-lived FD opened in
|
|
19
|
+
* append mode (`O_APPEND`). Sync IO sidesteps the buffer-flush
|
|
20
|
+
* race that bites WriteStream-based rotation (`clearSession`
|
|
21
|
+
* followed immediately by a new append). The blocking cost is
|
|
22
|
+
* negligible at our message rate (1-2 records per ~5-10s task).
|
|
23
|
+
* - `O_APPEND` guarantees every kernel write seeks to EOF
|
|
24
|
+
* atomically, so concurrent appenders on the same file (rare —
|
|
25
|
+
* would only happen if two device-agent siblings shared a
|
|
26
|
+
* sessions dir) never interleave a single line.
|
|
27
|
+
*
|
|
28
|
+
* Crash semantics:
|
|
29
|
+
*
|
|
30
|
+
* - Each write hits disk before the call returns; finalised turns
|
|
31
|
+
* survive an unclean shutdown. The user-selected `memory_only`
|
|
32
|
+
* in-flight policy means a turn that hadn't yet reached `task:end`
|
|
33
|
+
* is lost — that's expected.
|
|
34
|
+
* - Reads tolerate partial / corrupt tail lines (the writer might
|
|
35
|
+
* have died mid-line on a previous run) by skipping any line that
|
|
36
|
+
* fails to parse. Same convention as beeos-claw's
|
|
37
|
+
* `LocalSessionHistory.readEntries`.
|
|
38
|
+
*/
|
|
39
|
+
import { MessageStore, type MessageStoreOptions, type PersistedAssistantTurn, type UserMessageRecord } from "./message-store.js";
|
|
40
|
+
import type { AssistantTurnRecord } from "./in-flight-turn.js";
|
|
41
|
+
export interface JsonlMessageStoreOptions extends MessageStoreOptions {
|
|
42
|
+
/** Directory where `<sid>.jsonl` and `<sid>.meta.json` live. */
|
|
43
|
+
sessionsDir: string;
|
|
44
|
+
/**
|
|
45
|
+
* Specific sids to preload eagerly. Optional — when omitted, the
|
|
46
|
+
* store scans `sessionsDir` for every `*.jsonl` file. Production
|
|
47
|
+
* callers (single-session device-agent) pass the fixed sid here so
|
|
48
|
+
* `open` is O(1) regardless of how many other sids share the dir.
|
|
49
|
+
*/
|
|
50
|
+
preloadSids?: readonly string[];
|
|
51
|
+
/** Logger — only used to warn on parse failures. Optional. */
|
|
52
|
+
logger?: {
|
|
53
|
+
warn: (...args: unknown[]) => void;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Persistent variant of `MessageStore` — every append is mirrored to a
|
|
58
|
+
* per-session JSONL log, every metadata change debounced into
|
|
59
|
+
* `<sid>.meta.json`.
|
|
60
|
+
*
|
|
61
|
+
* Construct via `JsonlMessageStore.open(...)` so the on-disk state can
|
|
62
|
+
* be loaded into memory before any reads happen.
|
|
63
|
+
*/
|
|
64
|
+
export declare class JsonlMessageStore extends MessageStore {
|
|
65
|
+
private readonly sessionsDir;
|
|
66
|
+
private readonly logger?;
|
|
67
|
+
/** Lazily-opened append-mode FDs keyed by sessionId. */
|
|
68
|
+
private readonly fds;
|
|
69
|
+
private readonly metaTimers;
|
|
70
|
+
private closed;
|
|
71
|
+
private constructor();
|
|
72
|
+
/** Construct and load on-disk state into memory. */
|
|
73
|
+
static open(opts: JsonlMessageStoreOptions): Promise<JsonlMessageStore>;
|
|
74
|
+
/** Path of the JSONL transcript for a given sid. */
|
|
75
|
+
private sessionFile;
|
|
76
|
+
/** Path of the metadata file for a given sid. */
|
|
77
|
+
private metaFile;
|
|
78
|
+
/** Discover every sid present on disk by scanning `*.jsonl` files. */
|
|
79
|
+
private discoverSids;
|
|
80
|
+
/**
|
|
81
|
+
* Read the JSONL for a sid into memory. Skips rows that fail to
|
|
82
|
+
* parse (truncated tail line on a previous crash, schema mismatch
|
|
83
|
+
* from a future version, etc.) so a single bad row doesn't sink the
|
|
84
|
+
* whole session.
|
|
85
|
+
*/
|
|
86
|
+
private preloadSession;
|
|
87
|
+
private readMeta;
|
|
88
|
+
private getOrCreateFd;
|
|
89
|
+
appendUserMessage(sessionId: string, content: string, opts?: {
|
|
90
|
+
turnId?: string;
|
|
91
|
+
messageId?: string;
|
|
92
|
+
now?: number;
|
|
93
|
+
}): UserMessageRecord;
|
|
94
|
+
appendAssistantTurn(sessionId: string, turn: AssistantTurnRecord, opts?: {
|
|
95
|
+
now?: number;
|
|
96
|
+
}): PersistedAssistantTurn;
|
|
97
|
+
upsertSession(sessionId: string, title?: string): void;
|
|
98
|
+
renameSession(sessionId: string, title: string): void;
|
|
99
|
+
clearSession(sessionId: string): void;
|
|
100
|
+
deleteSession(sessionId: string): void;
|
|
101
|
+
/** Flush every pending meta write and close every open FD. */
|
|
102
|
+
close(): Promise<void>;
|
|
103
|
+
private writeJsonlLine;
|
|
104
|
+
private closeFd;
|
|
105
|
+
private rotateStream;
|
|
106
|
+
private scheduleMetaWrite;
|
|
107
|
+
private snapshotMeta;
|
|
108
|
+
private writeMetaNow;
|
|
109
|
+
}
|