@the-open-engine/zeroshot 6.39.2 → 6.40.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/README.md +12 -0
- package/cli/agent-provider-boundary.js +19 -0
- package/cli/agent-provider-boundary.ts +79 -0
- package/cli/export-stream.js +94 -0
- package/cli/export-stream.ts +118 -0
- package/cli/index.js +102 -54
- package/cli/json-export.js +2 -38
- package/cli/json-export.ts +2 -56
- package/cli/semantic-canonical.js +60 -0
- package/cli/semantic-canonical.ts +59 -0
- package/cli/semantic-contract.js +97 -0
- package/cli/semantic-contract.ts +153 -0
- package/cli/semantic-events.js +53 -0
- package/cli/semantic-events.ts +65 -0
- package/cli/semantic-evidence.js +63 -0
- package/cli/semantic-evidence.ts +65 -0
- package/cli/semantic-export.js +186 -0
- package/cli/semantic-export.ts +245 -0
- package/cli/semantic-json.js +69 -0
- package/cli/semantic-json.ts +69 -0
- package/cli/semantic-line-scanner.js +56 -0
- package/cli/semantic-line-scanner.ts +54 -0
- package/cli/semantic-parser.js +78 -0
- package/cli/semantic-parser.ts +95 -0
- package/cli/semantic-provider-line.js +91 -0
- package/cli/semantic-provider-line.ts +106 -0
- package/cli/trace-evidence.js +86 -0
- package/cli/trace-evidence.ts +115 -0
- package/cli/trace-export.js +120 -0
- package/cli/trace-export.ts +164 -0
- package/cli/trace-output-record.js +15 -0
- package/cli/trace-output-record.ts +18 -0
- package/cli/trace-output.js +98 -0
- package/cli/trace-output.ts +119 -0
- package/lib/agent-cli-provider/log-prefix.d.ts.map +1 -1
- package/lib/agent-cli-provider/log-prefix.js +8 -6
- package/lib/agent-cli-provider/log-prefix.js.map +1 -1
- package/lib/stream-json-parser.js +6 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +6 -4
- package/src/agent/agent-task-executor.js +36 -40
- package/src/agent/output-extraction-json.js +5 -6
- package/src/agent/output-extraction-json.ts +4 -7
- package/src/agent-cli-provider/log-prefix.ts +12 -7
- package/src/claude-task-runner.js +9 -9
- package/src/legacy-lib/stream-json-parser.ts +10 -5
- package/src/providers/index.js +1 -19
- package/src/task-log-line.d.ts +20 -0
- package/src/task-log-line.js +80 -0
- package/task-lib/commands/logs.js +5 -8
- package/task-lib/sdk-watcher-output.js +31 -0
- package/task-lib/sdk-watcher.js +4 -11
- package/task-lib/watcher-output-runtime.js +30 -16
package/README.md
CHANGED
|
@@ -146,6 +146,8 @@ zeroshot logs <id> -f # stream logs
|
|
|
146
146
|
zeroshot resume <id> [prompt] # resume a stopped or failed run
|
|
147
147
|
zeroshot stop <id> # graceful stop
|
|
148
148
|
zeroshot kill <id> # force stop
|
|
149
|
+
zeroshot export <id> --format trace --output run.trace.jsonl
|
|
150
|
+
zeroshot export <id> --format semantic --output run.semantic.jsonl
|
|
149
151
|
|
|
150
152
|
zeroshot providers # provider availability and defaults
|
|
151
153
|
zeroshot settings # effective settings
|
|
@@ -158,6 +160,16 @@ template supports `{{issue_number}}`, `{{issue_title}}`, and `{{issue_reference}
|
|
|
158
160
|
expand to empty text for tasks without an issue, so manual runs never emit `Closes #unknown`.
|
|
159
161
|
The unrendered template is retained for detached and resumed runs.
|
|
160
162
|
|
|
163
|
+
The `trace` export is a deterministic, provider-neutral research bundle. It preserves the ordered
|
|
164
|
+
cluster ledger, exact selected prompts, and exact raw task-log bytes without interpreting a Claude,
|
|
165
|
+
Codex, Pi, or other provider protocol. Missing evidence is recorded explicitly in its footer. File
|
|
166
|
+
exports are create-only: choose a new output path rather than replacing an existing bundle. Live
|
|
167
|
+
tasks are exported only as explicitly incomplete snapshots.
|
|
168
|
+
The separate `semantic` export runs those task bytes through Zeroshot's existing stateful provider
|
|
169
|
+
adapters and emits bounded `text`, `thinking`, `tool_call`, `tool_result`, and `result` events.
|
|
170
|
+
Zeroshot-owned wrapper and stderr records remain native-only. Parser diagnostics affect only
|
|
171
|
+
semantic completeness; they do not alter the native trace or run.
|
|
172
|
+
|
|
161
173
|
</details>
|
|
162
174
|
|
|
163
175
|
<details>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripTimestampPrefix = exports.getProviderAdapter = void 0;
|
|
4
|
+
function isRuntime(value, method) {
|
|
5
|
+
return (value !== null && typeof value === 'object' && typeof Reflect.get(value, method) === 'function');
|
|
6
|
+
}
|
|
7
|
+
function isAdapterRuntime(value) {
|
|
8
|
+
return isRuntime(value, 'getProviderAdapter');
|
|
9
|
+
}
|
|
10
|
+
function isPrefixRuntime(value) {
|
|
11
|
+
return isRuntime(value, 'stripTimestampPrefix');
|
|
12
|
+
}
|
|
13
|
+
const adapters = require('../lib/agent-cli-provider/adapters');
|
|
14
|
+
const prefixes = require('../lib/agent-cli-provider/log-prefix');
|
|
15
|
+
if (!isAdapterRuntime(adapters) || !isPrefixRuntime(prefixes)) {
|
|
16
|
+
throw new Error('Built agent-provider runtime is unavailable');
|
|
17
|
+
}
|
|
18
|
+
exports.getProviderAdapter = adapters.getProviderAdapter;
|
|
19
|
+
exports.stripTimestampPrefix = prefixes.stripTimestampPrefix;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
interface TextEvent {
|
|
2
|
+
readonly type: 'text' | 'thinking';
|
|
3
|
+
readonly text: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ToolCallEvent {
|
|
7
|
+
readonly type: 'tool_call';
|
|
8
|
+
readonly toolName: string | null | undefined;
|
|
9
|
+
readonly toolId: string | null | undefined;
|
|
10
|
+
readonly input: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ToolResultEvent {
|
|
14
|
+
readonly type: 'tool_result';
|
|
15
|
+
readonly toolId: string | null | undefined;
|
|
16
|
+
readonly content: unknown;
|
|
17
|
+
readonly isError: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResultEvent {
|
|
21
|
+
readonly type: 'result';
|
|
22
|
+
readonly success: boolean;
|
|
23
|
+
readonly result?: unknown;
|
|
24
|
+
readonly error?: unknown;
|
|
25
|
+
readonly cost?: unknown;
|
|
26
|
+
readonly duration?: unknown;
|
|
27
|
+
readonly inputTokens?: number;
|
|
28
|
+
readonly outputTokens?: number;
|
|
29
|
+
readonly cacheReadInputTokens?: number;
|
|
30
|
+
readonly cacheCreationInputTokens?: number;
|
|
31
|
+
readonly modelUsage?: unknown;
|
|
32
|
+
readonly requests?: number;
|
|
33
|
+
readonly usageSource?: unknown;
|
|
34
|
+
readonly usageCompleteness?: unknown;
|
|
35
|
+
readonly invocation?: unknown;
|
|
36
|
+
readonly ompSdk?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type OutputEvent = TextEvent | ToolCallEvent | ToolResultEvent | ResultEvent;
|
|
40
|
+
export type ProviderParseResult = OutputEvent | readonly OutputEvent[] | null;
|
|
41
|
+
|
|
42
|
+
export interface ProviderAdapter {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
readonly adapterVersion: string;
|
|
45
|
+
createParserState(): object;
|
|
46
|
+
parseEvent(line: string, state: object): ProviderParseResult;
|
|
47
|
+
finishParsing?(state: object): ProviderParseResult;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface AdapterRuntime {
|
|
51
|
+
getProviderAdapter(provider: string): ProviderAdapter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface PrefixRuntime {
|
|
55
|
+
stripTimestampPrefix(line: string): string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRuntime(value: unknown, method: string): boolean {
|
|
59
|
+
return (
|
|
60
|
+
value !== null && typeof value === 'object' && typeof Reflect.get(value, method) === 'function'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isAdapterRuntime(value: unknown): value is AdapterRuntime {
|
|
65
|
+
return isRuntime(value, 'getProviderAdapter');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isPrefixRuntime(value: unknown): value is PrefixRuntime {
|
|
69
|
+
return isRuntime(value, 'stripTimestampPrefix');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const adapters: unknown = require('../lib/agent-cli-provider/adapters');
|
|
73
|
+
const prefixes: unknown = require('../lib/agent-cli-provider/log-prefix');
|
|
74
|
+
if (!isAdapterRuntime(adapters) || !isPrefixRuntime(prefixes)) {
|
|
75
|
+
throw new Error('Built agent-provider runtime is unavailable');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const getProviderAdapter = adapters.getProviderAdapter;
|
|
79
|
+
export const stripTimestampPrefix = prefixes.stripTimestampPrefix;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nullableString = nullableString;
|
|
4
|
+
exports.isRecord = isRecord;
|
|
5
|
+
exports.compareText = compareText;
|
|
6
|
+
exports.sameFileSnapshot = sameFileSnapshot;
|
|
7
|
+
exports.createExclusiveDestination = createExclusiveDestination;
|
|
8
|
+
exports.createReplacingDestination = createReplacingDestination;
|
|
9
|
+
exports.createRecordWriter = createRecordWriter;
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
function nullableString(value) {
|
|
13
|
+
return typeof value === 'string' ? value : null;
|
|
14
|
+
}
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function compareText(left, right) {
|
|
19
|
+
if (left < right)
|
|
20
|
+
return -1;
|
|
21
|
+
return left > right ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
function sameFileSnapshot(before, after) {
|
|
24
|
+
return (before.dev === after.dev &&
|
|
25
|
+
before.ino === after.ino &&
|
|
26
|
+
before.size === after.size &&
|
|
27
|
+
before.mtimeNs === after.mtimeNs &&
|
|
28
|
+
before.ctimeNs === after.ctimeNs);
|
|
29
|
+
}
|
|
30
|
+
function writeAll(fd, value, label) {
|
|
31
|
+
const bytes = Buffer.from(value);
|
|
32
|
+
let offset = 0;
|
|
33
|
+
while (offset < bytes.length) {
|
|
34
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
35
|
+
if (!Number.isInteger(written) || written <= 0) {
|
|
36
|
+
throw new Error(`${label} export destination stopped accepting bytes`);
|
|
37
|
+
}
|
|
38
|
+
offset += written;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function streamDestination(stdout, label) {
|
|
42
|
+
if (typeof stdout.fd === 'number' && Number.isInteger(stdout.fd)) {
|
|
43
|
+
const fd = stdout.fd;
|
|
44
|
+
return { close() { }, write: (value) => writeAll(fd, value, label) };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
close() { },
|
|
48
|
+
write(value) {
|
|
49
|
+
stdout.write(value);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function createExclusiveDestination(outputPath, stdout, label) {
|
|
54
|
+
if (!outputPath)
|
|
55
|
+
return streamDestination(stdout, label);
|
|
56
|
+
const flags = fs.constants.O_WRONLY |
|
|
57
|
+
fs.constants.O_CREAT |
|
|
58
|
+
fs.constants.O_EXCL |
|
|
59
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
60
|
+
const fd = fs.openSync(outputPath, flags, 0o600);
|
|
61
|
+
try {
|
|
62
|
+
fs.fchmodSync(fd, 0o600);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
fs.closeSync(fd);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, label) };
|
|
69
|
+
}
|
|
70
|
+
function createReplacingDestination(outputPath, stdout) {
|
|
71
|
+
if (!outputPath)
|
|
72
|
+
return streamDestination(stdout, 'JSON');
|
|
73
|
+
const fd = fs.openSync(outputPath, 'w');
|
|
74
|
+
return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, 'JSON') };
|
|
75
|
+
}
|
|
76
|
+
function createRecordWriter(destination) {
|
|
77
|
+
const digest = crypto.createHash('sha256');
|
|
78
|
+
let records = 0;
|
|
79
|
+
const encode = (record) => `${JSON.stringify(record)}\n`;
|
|
80
|
+
return {
|
|
81
|
+
get records() {
|
|
82
|
+
return records;
|
|
83
|
+
},
|
|
84
|
+
write(record) {
|
|
85
|
+
const line = encode(record);
|
|
86
|
+
digest.update(line);
|
|
87
|
+
destination.write(line);
|
|
88
|
+
records += 1;
|
|
89
|
+
},
|
|
90
|
+
finish(record) {
|
|
91
|
+
destination.write(encode({ ...record, preceding_records: records, records_sha256: digest.digest('hex') }));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import crypto = require('crypto');
|
|
2
|
+
import fs = require('fs');
|
|
3
|
+
|
|
4
|
+
export interface ExportStream {
|
|
5
|
+
readonly fd?: number;
|
|
6
|
+
write(value: string): unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Destination {
|
|
10
|
+
close(): void;
|
|
11
|
+
write(value: string): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RecordWriter {
|
|
15
|
+
readonly records: number;
|
|
16
|
+
finish(record: Record<string, unknown>): void;
|
|
17
|
+
write(record: Record<string, unknown>): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function nullableString(value: unknown): string | null {
|
|
21
|
+
return typeof value === 'string' ? value : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function compareText(left: string, right: string): number {
|
|
29
|
+
if (left < right) return -1;
|
|
30
|
+
return left > right ? 1 : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sameFileSnapshot(before: fs.BigIntStats, after: fs.BigIntStats): boolean {
|
|
34
|
+
return (
|
|
35
|
+
before.dev === after.dev &&
|
|
36
|
+
before.ino === after.ino &&
|
|
37
|
+
before.size === after.size &&
|
|
38
|
+
before.mtimeNs === after.mtimeNs &&
|
|
39
|
+
before.ctimeNs === after.ctimeNs
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeAll(fd: number, value: string, label: string): void {
|
|
44
|
+
const bytes = Buffer.from(value);
|
|
45
|
+
let offset = 0;
|
|
46
|
+
while (offset < bytes.length) {
|
|
47
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
48
|
+
if (!Number.isInteger(written) || written <= 0) {
|
|
49
|
+
throw new Error(`${label} export destination stopped accepting bytes`);
|
|
50
|
+
}
|
|
51
|
+
offset += written;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function streamDestination(stdout: ExportStream, label: string): Destination {
|
|
56
|
+
if (typeof stdout.fd === 'number' && Number.isInteger(stdout.fd)) {
|
|
57
|
+
const fd = stdout.fd;
|
|
58
|
+
return { close(): void {}, write: (value) => writeAll(fd, value, label) };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
close(): void {},
|
|
62
|
+
write(value): void {
|
|
63
|
+
stdout.write(value);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createExclusiveDestination(
|
|
69
|
+
outputPath: string | null | undefined,
|
|
70
|
+
stdout: ExportStream,
|
|
71
|
+
label: string
|
|
72
|
+
): Destination {
|
|
73
|
+
if (!outputPath) return streamDestination(stdout, label);
|
|
74
|
+
const flags =
|
|
75
|
+
fs.constants.O_WRONLY |
|
|
76
|
+
fs.constants.O_CREAT |
|
|
77
|
+
fs.constants.O_EXCL |
|
|
78
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
79
|
+
const fd = fs.openSync(outputPath, flags, 0o600);
|
|
80
|
+
try {
|
|
81
|
+
fs.fchmodSync(fd, 0o600);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
fs.closeSync(fd);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, label) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createReplacingDestination(
|
|
90
|
+
outputPath: string | null | undefined,
|
|
91
|
+
stdout: ExportStream
|
|
92
|
+
): Destination {
|
|
93
|
+
if (!outputPath) return streamDestination(stdout, 'JSON');
|
|
94
|
+
const fd = fs.openSync(outputPath, 'w');
|
|
95
|
+
return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, 'JSON') };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createRecordWriter(destination: Destination): RecordWriter {
|
|
99
|
+
const digest = crypto.createHash('sha256');
|
|
100
|
+
let records = 0;
|
|
101
|
+
const encode = (record: Record<string, unknown>): string => `${JSON.stringify(record)}\n`;
|
|
102
|
+
return {
|
|
103
|
+
get records(): number {
|
|
104
|
+
return records;
|
|
105
|
+
},
|
|
106
|
+
write(record): void {
|
|
107
|
+
const line = encode(record);
|
|
108
|
+
digest.update(line);
|
|
109
|
+
destination.write(line);
|
|
110
|
+
records += 1;
|
|
111
|
+
},
|
|
112
|
+
finish(record): void {
|
|
113
|
+
destination.write(
|
|
114
|
+
encode({ ...record, preceding_records: records, records_sha256: digest.digest('hex') })
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -59,6 +59,7 @@ const {
|
|
|
59
59
|
} = require('../lib/provider-names');
|
|
60
60
|
const { commandExists } = require('../lib/provider-detection');
|
|
61
61
|
const { getProvider, parseProviderChunk } = require('../src/providers');
|
|
62
|
+
const { decodeTaskLogLine } = require('../src/task-log-line');
|
|
62
63
|
const { readClustersFileSync } = require('../lib/clusters-registry');
|
|
63
64
|
const { MOUNT_PRESETS, resolveEnvs } = require('../lib/docker-config');
|
|
64
65
|
const {
|
|
@@ -1430,19 +1431,10 @@ function startClusterDbPolling(cluster, clusterId, isActive, options) {
|
|
|
1430
1431
|
}
|
|
1431
1432
|
|
|
1432
1433
|
function parseIncrementalTaskLogLine(line) {
|
|
1433
|
-
const
|
|
1434
|
-
if (!
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
let timestamp = Date.now();
|
|
1439
|
-
let jsonContent = trimmed;
|
|
1440
|
-
|
|
1441
|
-
const timestampMatch = jsonContent.match(/^\[(\d{13})\](.*)$/);
|
|
1442
|
-
if (timestampMatch) {
|
|
1443
|
-
timestamp = parseInt(timestampMatch[1], 10);
|
|
1444
|
-
jsonContent = timestampMatch[2];
|
|
1445
|
-
}
|
|
1434
|
+
const decoded = decodeTaskLogLine(line.trim());
|
|
1435
|
+
if (!decoded.providerOutput) return null;
|
|
1436
|
+
const timestamp = decoded.timestamp ?? Date.now();
|
|
1437
|
+
const jsonContent = decoded.content.trim();
|
|
1446
1438
|
|
|
1447
1439
|
if (!jsonContent.startsWith('{')) {
|
|
1448
1440
|
return null;
|
|
@@ -2619,37 +2611,7 @@ function findAgentFromLifecycle(agents, taskId, lifecycleMessages) {
|
|
|
2619
2611
|
}
|
|
2620
2612
|
|
|
2621
2613
|
function parseTaskLogLine(line) {
|
|
2622
|
-
|
|
2623
|
-
if (!trimmed.startsWith('[')) {
|
|
2624
|
-
return null;
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
const parsed = parseTimestampedLine(trimmed);
|
|
2628
|
-
if (!parsed.jsonContent.startsWith('{')) {
|
|
2629
|
-
return null;
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
try {
|
|
2633
|
-
const json = JSON.parse(parsed.jsonContent);
|
|
2634
|
-
if (json.type === 'system' && json.subtype === 'init') {
|
|
2635
|
-
return null;
|
|
2636
|
-
}
|
|
2637
|
-
} catch {
|
|
2638
|
-
return null;
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
return parsed;
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
function parseTimestampedLine(line) {
|
|
2645
|
-
const timestampMatch = line.match(/^\[(\d{13})\](.*)$/);
|
|
2646
|
-
if (!timestampMatch) {
|
|
2647
|
-
return { timestamp: Date.now(), jsonContent: line };
|
|
2648
|
-
}
|
|
2649
|
-
return {
|
|
2650
|
-
timestamp: parseInt(timestampMatch[1], 10),
|
|
2651
|
-
jsonContent: timestampMatch[2],
|
|
2652
|
-
};
|
|
2614
|
+
return parseIncrementalTaskLogLine(line);
|
|
2653
2615
|
}
|
|
2654
2616
|
|
|
2655
2617
|
function buildTaskLogMessage({ taskId, timestamp, jsonContent, cluster, agent, iteration }) {
|
|
@@ -3405,9 +3367,9 @@ program
|
|
|
3405
3367
|
.command('export <cluster-id>')
|
|
3406
3368
|
.helpGroup('Maintenance:')
|
|
3407
3369
|
.description('Export cluster conversation')
|
|
3408
|
-
.option('-f, --format <format>', 'Export format: json, markdown, html', 'html')
|
|
3370
|
+
.option('-f, --format <format>', 'Export format: json, trace, semantic, markdown, html', 'html')
|
|
3409
3371
|
.option('-o, --output <file>', 'Output file (auto-generated for html)')
|
|
3410
|
-
.action((clusterId, options) => {
|
|
3372
|
+
.action(async (clusterId, options) => {
|
|
3411
3373
|
try {
|
|
3412
3374
|
// Get messages from DB
|
|
3413
3375
|
const Ledger = require('../src/ledger');
|
|
@@ -3418,14 +3380,7 @@ program
|
|
|
3418
3380
|
throw new Error(`Cluster ${clusterId} not found (no DB file)`);
|
|
3419
3381
|
}
|
|
3420
3382
|
|
|
3421
|
-
|
|
3422
|
-
if (options.format === 'json') {
|
|
3423
|
-
exportClusterJson(dbPath, clusterId, options.output || null);
|
|
3424
|
-
if (options.output) {
|
|
3425
|
-
console.log(`Exported to ${options.output}`);
|
|
3426
|
-
}
|
|
3427
|
-
return;
|
|
3428
|
-
}
|
|
3383
|
+
if (await exportClusterDataFormat(dbPath, clusterId, options)) return;
|
|
3429
3384
|
|
|
3430
3385
|
const ledger = new Ledger(dbPath);
|
|
3431
3386
|
const messages = ledger.getAll(clusterId);
|
|
@@ -6202,6 +6157,98 @@ function exportClusterJson(dbPath, clusterId, outputPath) {
|
|
|
6202
6157
|
);
|
|
6203
6158
|
}
|
|
6204
6159
|
|
|
6160
|
+
function tryExportClusterTraceSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
|
|
6161
|
+
const Ledger = require('../src/ledger');
|
|
6162
|
+
const { streamClusterTraceExport } = require('./trace-export');
|
|
6163
|
+
const ledger = new Ledger(dbPath, { readonly: true });
|
|
6164
|
+
try {
|
|
6165
|
+
return ledger.withReadSnapshot(() => {
|
|
6166
|
+
if (ledger.needsAgentOutputReconciliation(clusterId)) return false;
|
|
6167
|
+
streamClusterTraceExport({
|
|
6168
|
+
ledger,
|
|
6169
|
+
clusterId,
|
|
6170
|
+
readTask,
|
|
6171
|
+
allowedLogRoot,
|
|
6172
|
+
outputPath,
|
|
6173
|
+
});
|
|
6174
|
+
return true;
|
|
6175
|
+
});
|
|
6176
|
+
} finally {
|
|
6177
|
+
ledger.close();
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
function exportClusterTrace(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
|
|
6182
|
+
const Ledger = require('../src/ledger');
|
|
6183
|
+
const maxReconciliationAttempts = 3;
|
|
6184
|
+
for (let attempt = 0; attempt <= maxReconciliationAttempts; attempt += 1) {
|
|
6185
|
+
if (tryExportClusterTraceSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot)) {
|
|
6186
|
+
return;
|
|
6187
|
+
}
|
|
6188
|
+
if (attempt === maxReconciliationAttempts) break;
|
|
6189
|
+
const reconciliationLedger = new Ledger(dbPath);
|
|
6190
|
+
reconciliationLedger.close();
|
|
6191
|
+
}
|
|
6192
|
+
throw new Error(
|
|
6193
|
+
`Cluster ${clusterId} output changed during ${maxReconciliationAttempts} reconciliation attempts`
|
|
6194
|
+
);
|
|
6195
|
+
}
|
|
6196
|
+
|
|
6197
|
+
function tryExportClusterSemanticSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
|
|
6198
|
+
const Ledger = require('../src/ledger');
|
|
6199
|
+
const { streamClusterSemanticExport } = require('./semantic-export');
|
|
6200
|
+
const ledger = new Ledger(dbPath, { readonly: true });
|
|
6201
|
+
try {
|
|
6202
|
+
return ledger.withReadSnapshot(() => {
|
|
6203
|
+
if (ledger.needsAgentOutputReconciliation(clusterId)) return false;
|
|
6204
|
+
streamClusterSemanticExport({
|
|
6205
|
+
ledger,
|
|
6206
|
+
clusterId,
|
|
6207
|
+
readTask,
|
|
6208
|
+
allowedLogRoot,
|
|
6209
|
+
outputPath,
|
|
6210
|
+
});
|
|
6211
|
+
return true;
|
|
6212
|
+
});
|
|
6213
|
+
} finally {
|
|
6214
|
+
ledger.close();
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
|
|
6218
|
+
function exportClusterSemantic(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
|
|
6219
|
+
const Ledger = require('../src/ledger');
|
|
6220
|
+
const maxReconciliationAttempts = 3;
|
|
6221
|
+
for (let attempt = 0; attempt <= maxReconciliationAttempts; attempt += 1) {
|
|
6222
|
+
if (tryExportClusterSemanticSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot)) {
|
|
6223
|
+
return;
|
|
6224
|
+
}
|
|
6225
|
+
if (attempt === maxReconciliationAttempts) break;
|
|
6226
|
+
const reconciliationLedger = new Ledger(dbPath);
|
|
6227
|
+
reconciliationLedger.close();
|
|
6228
|
+
}
|
|
6229
|
+
throw new Error(
|
|
6230
|
+
`Cluster ${clusterId} output changed during ${maxReconciliationAttempts} reconciliation attempts`
|
|
6231
|
+
);
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
async function exportClusterDataFormat(dbPath, clusterId, options) {
|
|
6235
|
+
if (options.format === 'json') {
|
|
6236
|
+
exportClusterJson(dbPath, clusterId, options.output || null);
|
|
6237
|
+
if (options.output) console.log(`Exported to ${options.output}`);
|
|
6238
|
+
return true;
|
|
6239
|
+
}
|
|
6240
|
+
if (options.format !== 'trace' && options.format !== 'semantic') return false;
|
|
6241
|
+
|
|
6242
|
+
const [{ getTask }, { LOGS_DIR }] = await Promise.all([
|
|
6243
|
+
import('../task-lib/store.js'),
|
|
6244
|
+
import('../task-lib/config.js'),
|
|
6245
|
+
]);
|
|
6246
|
+
const exporter = options.format === 'trace' ? exportClusterTrace : exportClusterSemantic;
|
|
6247
|
+
exporter(dbPath, clusterId, options.output || null, getTask, LOGS_DIR);
|
|
6248
|
+
if (options.output) console.log(`Exported ${options.format} to ${options.output}`);
|
|
6249
|
+
return true;
|
|
6250
|
+
}
|
|
6251
|
+
|
|
6205
6252
|
// Main entry point
|
|
6206
6253
|
async function main() {
|
|
6207
6254
|
printLegacyDistroNotice();
|
|
@@ -6258,4 +6305,5 @@ module.exports = {
|
|
|
6258
6305
|
buildCompletionPrompt,
|
|
6259
6306
|
resolveRunMode,
|
|
6260
6307
|
killRunningClusters,
|
|
6308
|
+
parseTaskLogLine,
|
|
6261
6309
|
};
|
package/cli/json-export.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const
|
|
3
|
-
function isInteger(value) {
|
|
4
|
-
return Number.isInteger(value);
|
|
5
|
-
}
|
|
2
|
+
const export_stream_1 = require("./export-stream");
|
|
6
3
|
function indentJson(value, spaces) {
|
|
7
4
|
const prefix = ' '.repeat(spaces);
|
|
8
5
|
return JSON.stringify(value, null, 2)
|
|
@@ -10,41 +7,8 @@ function indentJson(value, spaces) {
|
|
|
10
7
|
.map((line) => `${prefix}${line}`)
|
|
11
8
|
.join('\n');
|
|
12
9
|
}
|
|
13
|
-
function writeAll(fd, value) {
|
|
14
|
-
const bytes = Buffer.from(value);
|
|
15
|
-
let offset = 0;
|
|
16
|
-
while (offset < bytes.length) {
|
|
17
|
-
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
18
|
-
if (!Number.isInteger(written) || written <= 0) {
|
|
19
|
-
throw new Error('JSON export destination stopped accepting bytes');
|
|
20
|
-
}
|
|
21
|
-
offset += written;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function createDestination(outputPath, stdout) {
|
|
25
|
-
if (outputPath) {
|
|
26
|
-
const fd = fs.openSync(outputPath, 'w');
|
|
27
|
-
return {
|
|
28
|
-
close: () => fs.closeSync(fd),
|
|
29
|
-
write: (value) => writeAll(fd, value),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
if (isInteger(stdout.fd)) {
|
|
33
|
-
const fd = stdout.fd;
|
|
34
|
-
return {
|
|
35
|
-
close() { },
|
|
36
|
-
write: (value) => writeAll(fd, value),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
close() { },
|
|
41
|
-
write: (value) => {
|
|
42
|
-
stdout.write(value);
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
10
|
function streamClusterJsonExport({ ledger, clusterId, outputPath = null, stdout = process.stdout, }) {
|
|
47
|
-
const destination =
|
|
11
|
+
const destination = (0, export_stream_1.createReplacingDestination)(outputPath, stdout);
|
|
48
12
|
const iterator = ledger.iterateAll(clusterId);
|
|
49
13
|
try {
|
|
50
14
|
destination.write(`{\n "cluster_id": ${JSON.stringify(clusterId)},\n "messages": `);
|
package/cli/json-export.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createReplacingDestination, type ExportStream } from './export-stream';
|
|
2
2
|
|
|
3
3
|
interface LedgerMessageIterator extends Iterator<unknown> {
|
|
4
4
|
return?: () => IteratorResult<unknown>;
|
|
@@ -8,11 +8,6 @@ interface ClusterLedger {
|
|
|
8
8
|
iterateAll(clusterId: string): LedgerMessageIterator;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
interface ExportStream {
|
|
12
|
-
readonly fd?: number;
|
|
13
|
-
write(value: string): unknown;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
interface JsonExportOptions {
|
|
17
12
|
ledger: ClusterLedger;
|
|
18
13
|
clusterId: string;
|
|
@@ -20,15 +15,6 @@ interface JsonExportOptions {
|
|
|
20
15
|
stdout?: ExportStream;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
interface Destination {
|
|
24
|
-
close(): void;
|
|
25
|
-
write(value: string): void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isInteger(value: unknown): value is number {
|
|
29
|
-
return Number.isInteger(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
18
|
function indentJson(value: unknown, spaces: number): string {
|
|
33
19
|
const prefix = ' '.repeat(spaces);
|
|
34
20
|
return JSON.stringify(value, null, 2)
|
|
@@ -37,53 +23,13 @@ function indentJson(value: unknown, spaces: number): string {
|
|
|
37
23
|
.join('\n');
|
|
38
24
|
}
|
|
39
25
|
|
|
40
|
-
function writeAll(fd: number, value: string): void {
|
|
41
|
-
const bytes = Buffer.from(value);
|
|
42
|
-
let offset = 0;
|
|
43
|
-
while (offset < bytes.length) {
|
|
44
|
-
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
45
|
-
if (!Number.isInteger(written) || written <= 0) {
|
|
46
|
-
throw new Error('JSON export destination stopped accepting bytes');
|
|
47
|
-
}
|
|
48
|
-
offset += written;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function createDestination(
|
|
53
|
-
outputPath: string | null | undefined,
|
|
54
|
-
stdout: ExportStream
|
|
55
|
-
): Destination {
|
|
56
|
-
if (outputPath) {
|
|
57
|
-
const fd = fs.openSync(outputPath, 'w');
|
|
58
|
-
return {
|
|
59
|
-
close: () => fs.closeSync(fd),
|
|
60
|
-
write: (value) => writeAll(fd, value),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (isInteger(stdout.fd)) {
|
|
65
|
-
const fd = stdout.fd;
|
|
66
|
-
return {
|
|
67
|
-
close(): void {},
|
|
68
|
-
write: (value) => writeAll(fd, value),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
close(): void {},
|
|
74
|
-
write: (value): void => {
|
|
75
|
-
stdout.write(value);
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
26
|
function streamClusterJsonExport({
|
|
81
27
|
ledger,
|
|
82
28
|
clusterId,
|
|
83
29
|
outputPath = null,
|
|
84
30
|
stdout = process.stdout,
|
|
85
31
|
}: JsonExportOptions): void {
|
|
86
|
-
const destination =
|
|
32
|
+
const destination = createReplacingDestination(outputPath, stdout);
|
|
87
33
|
const iterator = ledger.iterateAll(clusterId);
|
|
88
34
|
try {
|
|
89
35
|
destination.write(`{\n "cluster_id": ${JSON.stringify(clusterId)},\n "messages": `);
|