@gencode/uni 0.3.1 → 0.4.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/CHANGELOG.md +19 -0
- package/README.md +104 -0
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/index.d.ts.map +1 -1
- package/dist/shared/src/index.js.map +1 -1
- package/dist/shared/src/session.d.ts +13 -0
- package/dist/shared/src/session.d.ts.map +1 -1
- package/dist/uni/src/artifacts/accumulator.d.ts +17 -0
- package/dist/uni/src/artifacts/accumulator.d.ts.map +1 -0
- package/dist/uni/src/artifacts/accumulator.js +73 -0
- package/dist/uni/src/artifacts/accumulator.js.map +1 -0
- package/dist/uni/src/artifacts/parser.d.ts +14 -0
- package/dist/uni/src/artifacts/parser.d.ts.map +1 -0
- package/dist/uni/src/artifacts/parser.js +151 -0
- package/dist/uni/src/artifacts/parser.js.map +1 -0
- package/dist/uni/src/artifacts/stream-filter.d.ts +16 -0
- package/dist/uni/src/artifacts/stream-filter.d.ts.map +1 -0
- package/dist/uni/src/artifacts/stream-filter.js +93 -0
- package/dist/uni/src/artifacts/stream-filter.js.map +1 -0
- package/dist/uni/src/artifacts/tool-inference.d.ts +11 -0
- package/dist/uni/src/artifacts/tool-inference.d.ts.map +1 -0
- package/dist/uni/src/artifacts/tool-inference.js +257 -0
- package/dist/uni/src/artifacts/tool-inference.js.map +1 -0
- package/dist/uni/src/events/normalizer.d.ts.map +1 -1
- package/dist/uni/src/events/normalizer.js +8 -1
- package/dist/uni/src/events/normalizer.js.map +1 -1
- package/dist/uni/src/events/provider-events.d.ts.map +1 -1
- package/dist/uni/src/events/provider-events.js +353 -56
- package/dist/uni/src/events/provider-events.js.map +1 -1
- package/dist/uni/src/program.d.ts.map +1 -1
- package/dist/uni/src/program.js +8 -4
- package/dist/uni/src/program.js.map +1 -1
- package/dist/uni/src/providers/claude-code.d.ts.map +1 -1
- package/dist/uni/src/providers/claude-code.js +6 -2
- package/dist/uni/src/providers/claude-code.js.map +1 -1
- package/dist/uni/src/providers/codex.d.ts.map +1 -1
- package/dist/uni/src/providers/codex.js +13 -7
- package/dist/uni/src/providers/codex.js.map +1 -1
- package/dist/uni/src/providers/opencode.d.ts.map +1 -1
- package/dist/uni/src/providers/opencode.js +6 -2
- package/dist/uni/src/providers/opencode.js.map +1 -1
- package/dist/uni/src/providers/pi.d.ts.map +1 -1
- package/dist/uni/src/providers/pi.js +14 -5
- package/dist/uni/src/providers/pi.js.map +1 -1
- package/dist/uni/src/providers/provider-state.d.ts +10 -0
- package/dist/uni/src/providers/provider-state.d.ts.map +1 -0
- package/dist/uni/src/providers/provider-state.js +50 -0
- package/dist/uni/src/providers/provider-state.js.map +1 -0
- package/dist/uni/src/providers/spawned-provider.d.ts.map +1 -1
- package/dist/uni/src/providers/spawned-provider.js +36 -4
- package/dist/uni/src/providers/spawned-provider.js.map +1 -1
- package/dist/uni/src/run.d.ts.map +1 -1
- package/dist/uni/src/run.js +852 -336
- package/dist/uni/src/run.js.map +1 -1
- package/dist/uni/src/session/artifact-operation-store.d.ts +66 -0
- package/dist/uni/src/session/artifact-operation-store.d.ts.map +1 -0
- package/dist/uni/src/session/artifact-operation-store.js +370 -0
- package/dist/uni/src/session/artifact-operation-store.js.map +1 -0
- package/dist/uni/src/session/context-store.d.ts +37 -0
- package/dist/uni/src/session/context-store.d.ts.map +1 -0
- package/dist/uni/src/session/context-store.js +222 -0
- package/dist/uni/src/session/context-store.js.map +1 -0
- package/dist/uni/src/session/session-store.d.ts +57 -2
- package/dist/uni/src/session/session-store.d.ts.map +1 -1
- package/dist/uni/src/session/session-store.js +484 -15
- package/dist/uni/src/session/session-store.js.map +1 -1
- package/dist/uni/src/transcript/turn-accumulator.d.ts +57 -0
- package/dist/uni/src/transcript/turn-accumulator.d.ts.map +1 -0
- package/dist/uni/src/transcript/turn-accumulator.js +136 -0
- package/dist/uni/src/transcript/turn-accumulator.js.map +1 -0
- package/dist/uni/src/types.d.ts +25 -1
- package/dist/uni/src/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/uni/src/run.js
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { isAgentDiagnosticEvent } from "@gencode/shared";
|
|
4
|
+
import { ArtifactAccumulator } from "./artifacts/accumulator.js";
|
|
5
|
+
import { ARTIFACTS_START_TAG, parseAndStripArtifacts, } from "./artifacts/parser.js";
|
|
6
|
+
import { ArtifactStreamFilter } from "./artifacts/stream-filter.js";
|
|
7
|
+
import { inferArtifactsFromToolExecution } from "./artifacts/tool-inference.js";
|
|
2
8
|
import { generateApiKey } from "./auth/api-key.js";
|
|
3
9
|
import { normalizeProviderEvent } from "./events/normalizer.js";
|
|
4
10
|
import { UniHookDispatcher } from "./hooks.js";
|
|
11
|
+
import { providerStateDir, providerStateRoot } from "./providers/provider-state.js";
|
|
5
12
|
import { createBuiltInProviderRegistry } from "./providers/registry.js";
|
|
13
|
+
import { createArtifactOperationRecorder } from "./session/artifact-operation-store.js";
|
|
14
|
+
import { createUniContextStore, } from "./session/context-store.js";
|
|
6
15
|
import { resolveRunInput } from "./session/input.js";
|
|
7
16
|
import { initializeUniLogger, uniLogger } from "./session/logger.js";
|
|
8
17
|
import { createSessionState } from "./session/session-state.js";
|
|
9
|
-
import { appendUniTranscriptEntry, buildUniSessionMetadata, saveUniSessionMetadata, } from "./session/session-store.js";
|
|
18
|
+
import { appendUniTranscriptEntries, appendUniTranscriptEntry, bindUniProviderSession, buildUniSessionMetadata, loadUniSessionMetadata, prepareUniSession, saveUniSessionMetadata, UniSessionError, updateUniSessionMetadata, } from "./session/session-store.js";
|
|
10
19
|
import { CompositeSink } from "./transports/composite-sink.js";
|
|
11
20
|
import { HttpCallbackSink } from "./transports/http-callback-sink.js";
|
|
12
21
|
import { WebSocketSink } from "./transports/websocket-sink.js";
|
|
22
|
+
import { UniTurnAccumulator } from "./transcript/turn-accumulator.js";
|
|
13
23
|
const DEFAULT_STREAM_EVENTS = ["start", "text", "done", "error"];
|
|
14
24
|
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
25
|
+
const ZERO_USAGE = { input: 0, output: 0, total: 0 };
|
|
26
|
+
const ALIGNED_RUNTIME_AGENTS = new Set([
|
|
27
|
+
"claude-code",
|
|
28
|
+
"codex",
|
|
29
|
+
"opencode",
|
|
30
|
+
"pi",
|
|
31
|
+
]);
|
|
32
|
+
const UNI_ARTIFACT_DECLARATION_PROMPT = [
|
|
33
|
+
"## Artifact Declaration",
|
|
34
|
+
"Before your final answer, audit the current turn for user-relevant persistent files and user-openable URLs.",
|
|
35
|
+
"If this turn produced, modified, exported, downloaded, uploaded, shared, or published any such artifact, append exactly one JSON array block in this form:",
|
|
36
|
+
"<aimax_artifacts>",
|
|
37
|
+
'[{"kind":"file","file":"/absolute/or/workspace-relative/path","label":"short description"},{"kind":"url","url":"https://example.test/result","label":"short description"}]',
|
|
38
|
+
"</aimax_artifacts>",
|
|
39
|
+
"Audit successful write/edit/apply_patch operations, command outputs and redirections, plugin/skill results, online resources, and explicit child-agent artifacts.",
|
|
40
|
+
"Do not declare deleted, failed, read-only, temporary/cache, or internal AIMax session files.",
|
|
41
|
+
"If there are no artifacts, omit the block. Do not duplicate paths or URLs.",
|
|
42
|
+
"The block is for the runtime parser. It must be the last content in the answer, with no text after </aimax_artifacts>.",
|
|
43
|
+
].join("\n");
|
|
15
44
|
export const UNSUPPORTED_UNI_RUN_OPTION_MESSAGES = {
|
|
16
45
|
agent: "--agent is not supported by @gencode/uni yet",
|
|
17
46
|
skillsLoadPaths: "--skillsLoadPaths is not supported by @gencode/uni yet",
|
|
@@ -22,73 +51,62 @@ export const UNSUPPORTED_UNI_RUN_OPTION_MESSAGES = {
|
|
|
22
51
|
};
|
|
23
52
|
const UNSUPPORTED_UNI_RUN_OPTION_KEYS = Object.keys(UNSUPPORTED_UNI_RUN_OPTION_MESSAGES);
|
|
24
53
|
function hasUnsupportedOptionValue(value) {
|
|
25
|
-
if (value === undefined || value === null)
|
|
54
|
+
if (value === undefined || value === null)
|
|
26
55
|
return false;
|
|
27
|
-
|
|
28
|
-
if (typeof value === "string") {
|
|
56
|
+
if (typeof value === "string")
|
|
29
57
|
return value.trim().length > 0;
|
|
30
|
-
|
|
31
|
-
if (Array.isArray(value)) {
|
|
58
|
+
if (Array.isArray(value))
|
|
32
59
|
return value.length > 0;
|
|
33
|
-
}
|
|
34
60
|
return true;
|
|
35
61
|
}
|
|
36
62
|
function collectUnsupportedOptionKeys(options) {
|
|
37
63
|
const record = options;
|
|
38
|
-
|
|
39
|
-
for (const key of UNSUPPORTED_UNI_RUN_OPTION_KEYS) {
|
|
40
|
-
if (hasUnsupportedOptionValue(record[key])) {
|
|
41
|
-
unsupported.push(key);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return unsupported;
|
|
64
|
+
return UNSUPPORTED_UNI_RUN_OPTION_KEYS.filter((key) => hasUnsupportedOptionValue(record[key]));
|
|
45
65
|
}
|
|
46
66
|
export function omitUnsupportedUniRunOptions(options) {
|
|
47
67
|
const omittedKeys = collectUnsupportedOptionKeys(options);
|
|
48
|
-
if (omittedKeys.length === 0)
|
|
68
|
+
if (omittedKeys.length === 0)
|
|
49
69
|
return { options, omittedKeys };
|
|
50
|
-
}
|
|
51
70
|
const nextOptions = { ...options };
|
|
52
|
-
for (const key of omittedKeys)
|
|
71
|
+
for (const key of omittedKeys)
|
|
53
72
|
delete nextOptions[key];
|
|
54
|
-
}
|
|
55
73
|
return {
|
|
56
74
|
options: nextOptions,
|
|
57
75
|
omittedKeys,
|
|
58
76
|
};
|
|
59
77
|
}
|
|
60
78
|
function parseStreamEvents(value) {
|
|
61
|
-
if (!value?.trim())
|
|
79
|
+
if (!value?.trim())
|
|
62
80
|
return new Set(DEFAULT_STREAM_EVENTS);
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
const parsed = value.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
82
|
+
const allowed = new Set([
|
|
83
|
+
"start",
|
|
84
|
+
"text",
|
|
85
|
+
"progress",
|
|
86
|
+
"done",
|
|
87
|
+
"error",
|
|
88
|
+
"hitl",
|
|
89
|
+
"title_updated",
|
|
90
|
+
]);
|
|
69
91
|
const resolved = new Set();
|
|
70
92
|
for (const item of parsed) {
|
|
71
|
-
if (!allowed.has(item))
|
|
93
|
+
if (!allowed.has(item))
|
|
72
94
|
throw new Error(`Invalid stream event: ${item}`);
|
|
73
|
-
}
|
|
74
95
|
resolved.add(item);
|
|
75
96
|
}
|
|
76
97
|
return resolved.size > 0 ? resolved : new Set(DEFAULT_STREAM_EVENTS);
|
|
77
98
|
}
|
|
78
99
|
function coerceTimeout(value) {
|
|
79
|
-
if (value === undefined)
|
|
100
|
+
if (value === undefined)
|
|
80
101
|
return DEFAULT_TIMEOUT_MS;
|
|
81
|
-
}
|
|
82
102
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
83
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
103
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
84
104
|
throw new Error(`Invalid timeout: ${String(value)}`);
|
|
85
|
-
}
|
|
86
105
|
return parsed;
|
|
87
106
|
}
|
|
88
107
|
function coerceContextWindow(value) {
|
|
89
|
-
if (value === undefined)
|
|
108
|
+
if (value === undefined)
|
|
90
109
|
return undefined;
|
|
91
|
-
}
|
|
92
110
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
93
111
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
94
112
|
throw new Error(`Invalid context window: ${String(value)}`);
|
|
@@ -97,18 +115,16 @@ function coerceContextWindow(value) {
|
|
|
97
115
|
}
|
|
98
116
|
function ensureDataDir(value) {
|
|
99
117
|
const dataDir = value ?? process.env.AIMAX_DATA_DIR;
|
|
100
|
-
if (!dataDir)
|
|
118
|
+
if (!dataDir)
|
|
101
119
|
throw new Error("Data directory must be specified via dataDir or AIMAX_DATA_DIR");
|
|
102
|
-
}
|
|
103
120
|
return dataDir;
|
|
104
121
|
}
|
|
105
122
|
function ensureChannel(value) {
|
|
106
123
|
return value ?? "WEB";
|
|
107
124
|
}
|
|
108
125
|
function ensureApiKey(options) {
|
|
109
|
-
if (options.authToken)
|
|
126
|
+
if (options.authToken)
|
|
110
127
|
return generateApiKey(options.authToken);
|
|
111
|
-
}
|
|
112
128
|
return options.apiKey ?? process.env.AIMAX_API_KEY ?? "";
|
|
113
129
|
}
|
|
114
130
|
function assertSupportedOptions(options) {
|
|
@@ -117,6 +133,32 @@ function assertSupportedOptions(options) {
|
|
|
117
133
|
throw new Error(unsupported.map((key) => UNSUPPORTED_UNI_RUN_OPTION_MESSAGES[key]).join("; "));
|
|
118
134
|
}
|
|
119
135
|
}
|
|
136
|
+
function parseArtifactUrlWhitelist(value) {
|
|
137
|
+
const rawValues = Array.isArray(value) ? value : value?.split(",") ?? [];
|
|
138
|
+
const hostnames = rawValues.flatMap((raw) => {
|
|
139
|
+
const candidate = raw.trim();
|
|
140
|
+
if (!candidate)
|
|
141
|
+
return [];
|
|
142
|
+
try {
|
|
143
|
+
return [new URL(candidate).hostname.toLowerCase()];
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return [candidate.toLowerCase()];
|
|
147
|
+
}
|
|
148
|
+
}).filter(Boolean);
|
|
149
|
+
return hostnames.length > 0 ? [...new Set(hostnames)] : undefined;
|
|
150
|
+
}
|
|
151
|
+
function accumulateUsage(current, next) {
|
|
152
|
+
if (!next)
|
|
153
|
+
return current;
|
|
154
|
+
if (!current)
|
|
155
|
+
return { ...next };
|
|
156
|
+
return {
|
|
157
|
+
input: current.input + next.input,
|
|
158
|
+
output: current.output + next.output,
|
|
159
|
+
total: current.total + next.total,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
120
162
|
async function resolveRunConfig(options) {
|
|
121
163
|
assertSupportedOptions(options);
|
|
122
164
|
const input = await resolveRunInput(options);
|
|
@@ -124,7 +166,7 @@ async function resolveRunConfig(options) {
|
|
|
124
166
|
runtimeAgent: options.runtimeAgent,
|
|
125
167
|
dataDir: ensureDataDir(options.dataDir),
|
|
126
168
|
projectDir: options.projectDir?.trim() || undefined,
|
|
127
|
-
sessionId: options.sessionId?.trim() ||
|
|
169
|
+
sessionId: options.sessionId?.trim() || randomUUID(),
|
|
128
170
|
sessionStore: options.sessionStore?.trim() || "sessions",
|
|
129
171
|
messageId: options.messageId?.trim() || undefined,
|
|
130
172
|
channel: ensureChannel(options.channel),
|
|
@@ -138,6 +180,7 @@ async function resolveRunConfig(options) {
|
|
|
138
180
|
streamUrl: options.streamUrl?.trim() || undefined,
|
|
139
181
|
streamAuthToken: options.streamAuthToken?.trim() || undefined,
|
|
140
182
|
streamEvents: parseStreamEvents(options.streamEvents),
|
|
183
|
+
artifactsUrlWhitelist: parseArtifactUrlWhitelist(options.artifactsUrlWhitelist),
|
|
141
184
|
timeoutMs: coerceTimeout(options.timeout),
|
|
142
185
|
output: options.output === "json" || options.output === "jsonl" ? options.output : "text",
|
|
143
186
|
runtimeConfig: options.runtimeConfig ?? {},
|
|
@@ -148,32 +191,25 @@ async function resolveRunConfig(options) {
|
|
|
148
191
|
function createExternalSink(config) {
|
|
149
192
|
const sinks = [];
|
|
150
193
|
let websocketSink = null;
|
|
151
|
-
if (config.callbackUrl)
|
|
194
|
+
if (config.callbackUrl)
|
|
152
195
|
sinks.push(new HttpCallbackSink(config.callbackUrl));
|
|
153
|
-
}
|
|
154
196
|
if (config.streamUrl) {
|
|
155
197
|
websocketSink = new WebSocketSink(config.streamUrl, config.streamEvents, config.streamAuthToken);
|
|
156
198
|
sinks.push(websocketSink);
|
|
157
199
|
}
|
|
158
|
-
return {
|
|
159
|
-
sink: new CompositeSink(sinks),
|
|
160
|
-
websocketSink,
|
|
161
|
-
};
|
|
200
|
+
return { sink: new CompositeSink(sinks), websocketSink };
|
|
162
201
|
}
|
|
163
202
|
async function emitExternalEvent(sink, event, state, onExternalEvent) {
|
|
164
203
|
state.externalEvents.push(event);
|
|
165
204
|
await onExternalEvent?.(event);
|
|
166
205
|
await sink.send(event);
|
|
167
206
|
}
|
|
168
|
-
function eventMessageId(event,
|
|
169
|
-
return event.messageId ??
|
|
207
|
+
function eventMessageId(event, fallback) {
|
|
208
|
+
return event.messageId ?? fallback;
|
|
170
209
|
}
|
|
171
210
|
async function emitProgressEvent(params) {
|
|
172
211
|
const { progressEvent, state, sink, websocketSink, onProgress, onExternalEvent } = params;
|
|
173
212
|
await onProgress?.(progressEvent);
|
|
174
|
-
if (progressEvent.sessionId) {
|
|
175
|
-
state.sessionId = progressEvent.sessionId;
|
|
176
|
-
}
|
|
177
213
|
if (progressEvent.type === "stream_text_delta") {
|
|
178
214
|
state.finalText += progressEvent.text;
|
|
179
215
|
await websocketSink?.sendTextDelta({
|
|
@@ -184,12 +220,11 @@ async function emitProgressEvent(params) {
|
|
|
184
220
|
text: progressEvent.text,
|
|
185
221
|
});
|
|
186
222
|
}
|
|
187
|
-
if (progressEvent.type === "text") {
|
|
223
|
+
else if (progressEvent.type === "text") {
|
|
188
224
|
state.finalText += progressEvent.text;
|
|
189
225
|
}
|
|
190
|
-
if (isAgentDiagnosticEvent(progressEvent))
|
|
226
|
+
if (isAgentDiagnosticEvent(progressEvent))
|
|
191
227
|
return;
|
|
192
|
-
}
|
|
193
228
|
if (progressEvent.type === "start") {
|
|
194
229
|
await emitExternalEvent(sink, {
|
|
195
230
|
type: "start",
|
|
@@ -234,14 +269,411 @@ async function emitProgressEvent(params) {
|
|
|
234
269
|
event: progressEvent,
|
|
235
270
|
}, state, onExternalEvent);
|
|
236
271
|
}
|
|
272
|
+
function promptText(input) {
|
|
273
|
+
if (input.kind === "text")
|
|
274
|
+
return input.message;
|
|
275
|
+
return input.singleUserText ?? JSON.stringify(input.messages);
|
|
276
|
+
}
|
|
277
|
+
function providerInput(input, aligned) {
|
|
278
|
+
if (!aligned)
|
|
279
|
+
return input;
|
|
280
|
+
if (input.kind === "text") {
|
|
281
|
+
return { kind: "text", message: `${input.message}\n\n${UNI_ARTIFACT_DECLARATION_PROMPT}` };
|
|
282
|
+
}
|
|
283
|
+
const messages = [
|
|
284
|
+
...input.messages,
|
|
285
|
+
{ role: "user", content: UNI_ARTIFACT_DECLARATION_PROMPT },
|
|
286
|
+
];
|
|
287
|
+
return {
|
|
288
|
+
kind: "messages",
|
|
289
|
+
messages,
|
|
290
|
+
...(input.singleUserText
|
|
291
|
+
? { singleUserText: `${input.singleUserText}\n\n${UNI_ARTIFACT_DECLARATION_PROMPT}` }
|
|
292
|
+
: {}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function titleForPrompt(value) {
|
|
296
|
+
return value.length > 80 ? `${value.slice(0, 77)}...` : value;
|
|
297
|
+
}
|
|
298
|
+
function nonEmpty(value) {
|
|
299
|
+
return value?.trim() ? value.trim() : undefined;
|
|
300
|
+
}
|
|
301
|
+
function redactKnownProviderIds(value, providerIds) {
|
|
302
|
+
let redacted = value;
|
|
303
|
+
for (const providerId of providerIds) {
|
|
304
|
+
if (providerId) {
|
|
305
|
+
redacted = redacted.replaceAll(providerId, "[provider-session]");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return redacted;
|
|
309
|
+
}
|
|
310
|
+
function redactKnownProviderIdsInValue(value, providerIds, depth = 0) {
|
|
311
|
+
if (typeof value === "string") {
|
|
312
|
+
return redactKnownProviderIds(value, providerIds);
|
|
313
|
+
}
|
|
314
|
+
if (!value || typeof value !== "object" || depth > 12) {
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
if (Array.isArray(value)) {
|
|
318
|
+
return value.map((item) => redactKnownProviderIdsInValue(item, providerIds, depth + 1));
|
|
319
|
+
}
|
|
320
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
|
|
321
|
+
key,
|
|
322
|
+
redactKnownProviderIdsInValue(nested, providerIds, depth + 1),
|
|
323
|
+
]));
|
|
324
|
+
}
|
|
325
|
+
function redactProviderEventForPublicBoundary(event, providerIds) {
|
|
326
|
+
return redactKnownProviderIdsInValue(event, providerIds);
|
|
327
|
+
}
|
|
328
|
+
function providerStateRoots(config) {
|
|
329
|
+
const commonEnv = config.runtimeConfig.env ?? {};
|
|
330
|
+
switch (config.runtimeAgent) {
|
|
331
|
+
case "claude-code": {
|
|
332
|
+
const runtime = config.runtimeConfig.claudeCode;
|
|
333
|
+
return [
|
|
334
|
+
nonEmpty(runtime?.env?.CLAUDE_CONFIG_DIR)
|
|
335
|
+
?? nonEmpty(runtime?.provider?.env?.CLAUDE_CONFIG_DIR)
|
|
336
|
+
?? nonEmpty(commonEnv.CLAUDE_CONFIG_DIR)
|
|
337
|
+
?? nonEmpty(process.env.CLAUDE_CONFIG_DIR)
|
|
338
|
+
?? providerStateDir(config.dataDir, "claude-code"),
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
case "codex": {
|
|
342
|
+
const runtime = config.runtimeConfig.codex;
|
|
343
|
+
return [
|
|
344
|
+
nonEmpty(runtime?.codexHome)
|
|
345
|
+
?? nonEmpty(runtime?.env?.CODEX_HOME)
|
|
346
|
+
?? nonEmpty(runtime?.provider?.env?.CODEX_HOME)
|
|
347
|
+
?? nonEmpty(commonEnv.CODEX_HOME)
|
|
348
|
+
?? nonEmpty(process.env.CODEX_HOME)
|
|
349
|
+
?? providerStateDir(config.dataDir, "codex"),
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
case "opencode": {
|
|
353
|
+
const runtime = config.runtimeConfig.opencode;
|
|
354
|
+
const root = nonEmpty(runtime?.env?.XDG_DATA_HOME)
|
|
355
|
+
?? nonEmpty(runtime?.provider?.env?.XDG_DATA_HOME)
|
|
356
|
+
?? nonEmpty(commonEnv.XDG_DATA_HOME)
|
|
357
|
+
?? nonEmpty(process.env.XDG_DATA_HOME)
|
|
358
|
+
?? providerStateRoot(config.dataDir);
|
|
359
|
+
return [path.join(root, "opencode")];
|
|
360
|
+
}
|
|
361
|
+
case "pi": {
|
|
362
|
+
const runtime = config.runtimeConfig.pi;
|
|
363
|
+
const agentDir = nonEmpty(runtime?.env?.PI_CODING_AGENT_DIR)
|
|
364
|
+
?? nonEmpty(commonEnv.PI_CODING_AGENT_DIR)
|
|
365
|
+
?? nonEmpty(process.env.PI_CODING_AGENT_DIR)
|
|
366
|
+
?? providerStateDir(config.dataDir, "pi");
|
|
367
|
+
const sessionsDir = nonEmpty(runtime?.env?.PI_CODING_AGENT_SESSION_DIR)
|
|
368
|
+
?? nonEmpty(commonEnv.PI_CODING_AGENT_SESSION_DIR)
|
|
369
|
+
?? nonEmpty(process.env.PI_CODING_AGENT_SESSION_DIR)
|
|
370
|
+
?? path.join(agentDir, "sessions");
|
|
371
|
+
return [agentDir, sessionsDir];
|
|
372
|
+
}
|
|
373
|
+
case "acp":
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function configuredProviderModel(config) {
|
|
378
|
+
if (config.model)
|
|
379
|
+
return config.model;
|
|
380
|
+
switch (config.runtimeAgent) {
|
|
381
|
+
case "claude-code":
|
|
382
|
+
return nonEmpty(config.runtimeConfig.claudeCode?.provider?.model);
|
|
383
|
+
case "codex":
|
|
384
|
+
return nonEmpty(config.runtimeConfig.codex?.provider?.model);
|
|
385
|
+
case "opencode":
|
|
386
|
+
return nonEmpty(config.runtimeConfig.opencode?.provider?.model);
|
|
387
|
+
case "pi":
|
|
388
|
+
case "acp":
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function prepareAcpCompatibilitySession(params) {
|
|
393
|
+
const existing = await loadUniSessionMetadata(params.config.dataDir, params.config.sessionId, params.config.sessionStore);
|
|
394
|
+
if (!existing) {
|
|
395
|
+
await saveUniSessionMetadata(params.config.dataDir, buildUniSessionMetadata({
|
|
396
|
+
sessionId: params.config.sessionId,
|
|
397
|
+
title: params.title,
|
|
398
|
+
channel: params.config.channel,
|
|
399
|
+
}), params.config.sessionStore);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function toRecord(value) {
|
|
403
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
404
|
+
? value
|
|
405
|
+
: { input: value };
|
|
406
|
+
}
|
|
407
|
+
function canonicalToolName(name) {
|
|
408
|
+
const normalized = name.trim().toLowerCase().replaceAll("-", "_");
|
|
409
|
+
if (normalized === "write")
|
|
410
|
+
return "write_file";
|
|
411
|
+
if (normalized === "edit")
|
|
412
|
+
return "edit_file";
|
|
413
|
+
if (normalized === "command_execution" || normalized === "shell" || normalized === "bash_command") {
|
|
414
|
+
return "exec";
|
|
415
|
+
}
|
|
416
|
+
if (normalized === "file_change")
|
|
417
|
+
return "apply_patch";
|
|
418
|
+
return normalized;
|
|
419
|
+
}
|
|
420
|
+
function hasStringField(record, keys) {
|
|
421
|
+
return keys.some((key) => typeof record[key] === "string");
|
|
422
|
+
}
|
|
423
|
+
function isTrustedOpenCodeCompletedOnlyFileTool(event) {
|
|
424
|
+
if (event.isError || !event.output.trim())
|
|
425
|
+
return false;
|
|
426
|
+
const input = toRecord(event.input);
|
|
427
|
+
if (!firstString(input, ["path", "file_path", "filePath"]))
|
|
428
|
+
return false;
|
|
429
|
+
const name = canonicalToolName(event.name);
|
|
430
|
+
if (name === "write_file") {
|
|
431
|
+
return hasStringField(input, ["content"]);
|
|
432
|
+
}
|
|
433
|
+
if (name === "edit_file") {
|
|
434
|
+
return hasStringField(input, ["oldString", "old_string", "oldText", "old_text"])
|
|
435
|
+
&& hasStringField(input, ["newString", "new_string", "newText", "new_text"]);
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
function firstString(record, keys) {
|
|
440
|
+
for (const key of keys) {
|
|
441
|
+
const value = record[key];
|
|
442
|
+
if (typeof value === "string" && value.trim())
|
|
443
|
+
return value.trim();
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
function patchSummary(input) {
|
|
448
|
+
const added = [];
|
|
449
|
+
const modified = [];
|
|
450
|
+
const deleted = [];
|
|
451
|
+
const changes = input.changes;
|
|
452
|
+
if (Array.isArray(changes)) {
|
|
453
|
+
for (const raw of changes) {
|
|
454
|
+
if (!raw || typeof raw !== "object")
|
|
455
|
+
continue;
|
|
456
|
+
const change = raw;
|
|
457
|
+
const file = firstString(change, ["path", "file", "file_path"]);
|
|
458
|
+
if (!file)
|
|
459
|
+
continue;
|
|
460
|
+
const kind = firstString(change, ["kind", "type", "operation"])?.toLowerCase();
|
|
461
|
+
if (kind === "add" || kind === "create")
|
|
462
|
+
added.push(file);
|
|
463
|
+
else if (kind === "delete" || kind === "remove")
|
|
464
|
+
deleted.push(file);
|
|
465
|
+
else
|
|
466
|
+
modified.push(file);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const patch = firstString(input, ["patch", "input"]);
|
|
470
|
+
if (patch) {
|
|
471
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
472
|
+
const match = line.match(/^\*\*\* (Add|Update|Delete) File:\s*(.+)$/);
|
|
473
|
+
if (!match?.[2])
|
|
474
|
+
continue;
|
|
475
|
+
if (match[1] === "Add")
|
|
476
|
+
added.push(match[2].trim());
|
|
477
|
+
else if (match[1] === "Delete")
|
|
478
|
+
deleted.push(match[2].trim());
|
|
479
|
+
else
|
|
480
|
+
modified.push(match[2].trim());
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (added.length + modified.length + deleted.length === 0)
|
|
484
|
+
return undefined;
|
|
485
|
+
return { added, modified, deleted };
|
|
486
|
+
}
|
|
487
|
+
function structuredFileChangesPatch(input) {
|
|
488
|
+
const changes = toRecord(input).changes;
|
|
489
|
+
if (!Array.isArray(changes))
|
|
490
|
+
return undefined;
|
|
491
|
+
const markers = ["*** Begin Patch"];
|
|
492
|
+
for (const raw of changes) {
|
|
493
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
494
|
+
continue;
|
|
495
|
+
const change = raw;
|
|
496
|
+
const kind = firstString(change, ["kind", "type", "operation"])?.toLowerCase();
|
|
497
|
+
const file = firstString(change, ["path", "file", "file_path", "target"]);
|
|
498
|
+
const source = firstString(change, ["source", "source_path", "from", "old_path"]);
|
|
499
|
+
if ((kind === "move" || kind === "rename") && file && source) {
|
|
500
|
+
markers.push(`*** Update File: ${source}`, `*** Move to: ${file}`);
|
|
501
|
+
}
|
|
502
|
+
else if ((kind === "add" || kind === "create") && file) {
|
|
503
|
+
markers.push(`*** Add File: ${file}`);
|
|
504
|
+
}
|
|
505
|
+
else if ((kind === "delete" || kind === "remove") && file) {
|
|
506
|
+
markers.push(`*** Delete File: ${file}`);
|
|
507
|
+
}
|
|
508
|
+
else if (file) {
|
|
509
|
+
markers.push(`*** Update File: ${file}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
markers.push("*** End Patch");
|
|
513
|
+
return markers.length > 2 ? markers.join("\n") : undefined;
|
|
514
|
+
}
|
|
515
|
+
function artifactAuditStartEvent(event) {
|
|
516
|
+
if (canonicalToolName(event.name) !== "apply_patch")
|
|
517
|
+
return event;
|
|
518
|
+
const patch = structuredFileChangesPatch(event.input);
|
|
519
|
+
return patch ? { ...event, name: "apply_patch", input: { patch } } : event;
|
|
520
|
+
}
|
|
521
|
+
function artifactAuditEndEvent(event) {
|
|
522
|
+
if (canonicalToolName(event.name) !== "apply_patch")
|
|
523
|
+
return event;
|
|
524
|
+
const patch = structuredFileChangesPatch(event.input);
|
|
525
|
+
return patch ? { ...event, name: "apply_patch", input: { patch } } : event;
|
|
526
|
+
}
|
|
527
|
+
function artifactAuditFileOperation(event) {
|
|
528
|
+
const name = canonicalToolName(event.name);
|
|
529
|
+
if (name !== "apply_patch")
|
|
530
|
+
return { ...event, name };
|
|
531
|
+
const patch = structuredFileChangesPatch(event.input);
|
|
532
|
+
return {
|
|
533
|
+
...event,
|
|
534
|
+
name,
|
|
535
|
+
...(patch ? { input: { patch } } : {}),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function inferenceResultForTool(params) {
|
|
539
|
+
const input = toRecord(params.input);
|
|
540
|
+
const name = canonicalToolName(params.name);
|
|
541
|
+
const content = [{ type: "text", text: params.output }];
|
|
542
|
+
if (name === "write_file") {
|
|
543
|
+
return { content, details: { path: firstString(input, ["path", "file_path", "filePath"]) } };
|
|
544
|
+
}
|
|
545
|
+
if (name === "edit_file") {
|
|
546
|
+
return {
|
|
547
|
+
content,
|
|
548
|
+
details: {
|
|
549
|
+
path: firstString(input, ["path", "file_path", "filePath"]),
|
|
550
|
+
occurrences: params.isError ? 0 : 1,
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
if (name === "apply_patch")
|
|
555
|
+
return { content, details: { summary: patchSummary(input) } };
|
|
556
|
+
if (name === "exec" || name === "bash" || name === "process") {
|
|
557
|
+
return {
|
|
558
|
+
content,
|
|
559
|
+
details: {
|
|
560
|
+
status: params.isError ? "failed" : "completed",
|
|
561
|
+
cwd: firstString(input, ["cwd"]) ?? undefined,
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return params.output;
|
|
566
|
+
}
|
|
567
|
+
function inferenceResult(event) {
|
|
568
|
+
return inferenceResultForTool(event);
|
|
569
|
+
}
|
|
570
|
+
function isInternalArtifact(artifact, dataDir) {
|
|
571
|
+
if (artifact.kind !== "file")
|
|
572
|
+
return false;
|
|
573
|
+
const internalRoot = path.resolve(dataDir, ".aimax");
|
|
574
|
+
const candidate = path.resolve(artifact.file);
|
|
575
|
+
return candidate === internalRoot || candidate.startsWith(`${internalRoot}${path.sep}`);
|
|
576
|
+
}
|
|
577
|
+
function parseArtifactArray(value, workspaceDir) {
|
|
578
|
+
if (!Array.isArray(value))
|
|
579
|
+
return [];
|
|
580
|
+
const now = new Date().toISOString();
|
|
581
|
+
return value.flatMap((raw) => {
|
|
582
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
583
|
+
return [];
|
|
584
|
+
const record = raw;
|
|
585
|
+
const label = typeof record.label === "string" && record.label.trim()
|
|
586
|
+
? record.label.trim()
|
|
587
|
+
: undefined;
|
|
588
|
+
if (record.kind === "file" && typeof record.file === "string" && record.file.trim() && label) {
|
|
589
|
+
const file = path.isAbsolute(record.file) ? path.normalize(record.file) : path.resolve(workspaceDir, record.file);
|
|
590
|
+
return [{ kind: "file", file, label, timestamp: now }];
|
|
591
|
+
}
|
|
592
|
+
if (record.kind === "url" && typeof record.url === "string" && record.url.trim() && label) {
|
|
593
|
+
try {
|
|
594
|
+
const url = new URL(record.url).toString();
|
|
595
|
+
return [{ kind: "url", url, label, timestamp: now }];
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return [];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return [];
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function extractChildArtifacts(toolName, output, workspaceDir) {
|
|
605
|
+
const normalized = toolName.trim().toLowerCase().replaceAll("-", "_");
|
|
606
|
+
if (!normalized.includes("subagent") && normalized !== "task" && normalized !== "batch")
|
|
607
|
+
return [];
|
|
608
|
+
let parsed;
|
|
609
|
+
try {
|
|
610
|
+
parsed = JSON.parse(output);
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
616
|
+
return [];
|
|
617
|
+
const record = parsed;
|
|
618
|
+
const direct = parseArtifactArray(record.artifacts, workspaceDir);
|
|
619
|
+
if (direct.length > 0)
|
|
620
|
+
return direct;
|
|
621
|
+
const result = record.result;
|
|
622
|
+
return result && typeof result === "object" && !Array.isArray(result)
|
|
623
|
+
? parseArtifactArray(result.artifacts, workspaceDir)
|
|
624
|
+
: [];
|
|
625
|
+
}
|
|
237
626
|
export async function runUni(options) {
|
|
238
627
|
const config = await resolveRunConfig(options);
|
|
239
628
|
initializeUniLogger(config.dataDir);
|
|
240
629
|
const registry = createBuiltInProviderRegistry();
|
|
241
630
|
const provider = options.runtimeProviderFactory?.(config.runtimeAgent) ?? registry.get(config.runtimeAgent);
|
|
242
|
-
if (!provider)
|
|
631
|
+
if (!provider)
|
|
243
632
|
throw new Error(`Unsupported runtime agent: ${config.runtimeAgent}`);
|
|
633
|
+
const aligned = ALIGNED_RUNTIME_AGENTS.has(config.runtimeAgent);
|
|
634
|
+
const rawPrompt = promptText(config.input);
|
|
635
|
+
const metadataTitle = titleForPrompt(rawPrompt);
|
|
636
|
+
let prepared;
|
|
637
|
+
let contextStore;
|
|
638
|
+
if (aligned) {
|
|
639
|
+
try {
|
|
640
|
+
prepared = await prepareUniSession({
|
|
641
|
+
dataDir: config.dataDir,
|
|
642
|
+
logicalSessionId: config.sessionId,
|
|
643
|
+
runtimeAgent: config.runtimeAgent,
|
|
644
|
+
title: metadataTitle,
|
|
645
|
+
channel: config.channel,
|
|
646
|
+
storeName: config.sessionStore,
|
|
647
|
+
providerStateRoots: providerStateRoots(config),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
if (error instanceof UniSessionError && error.code === "legacy_unresolved") {
|
|
652
|
+
await createUniContextStore({
|
|
653
|
+
dataDir: config.dataDir,
|
|
654
|
+
logicalSessionId: config.sessionId,
|
|
655
|
+
storeName: config.sessionStore,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
contextStore = await createUniContextStore({
|
|
661
|
+
dataDir: config.dataDir,
|
|
662
|
+
logicalSessionId: config.sessionId,
|
|
663
|
+
storeName: config.sessionStore,
|
|
664
|
+
});
|
|
244
665
|
}
|
|
666
|
+
else {
|
|
667
|
+
await prepareAcpCompatibilitySession({ config, title: metadataTitle });
|
|
668
|
+
}
|
|
669
|
+
await appendUniTranscriptEntry(config.dataDir, config.sessionId, {
|
|
670
|
+
role: "user",
|
|
671
|
+
content: rawPrompt,
|
|
672
|
+
timestamp: new Date().toISOString(),
|
|
673
|
+
runtimeAgent: config.runtimeAgent,
|
|
674
|
+
...(config.messageId ? { messageId: config.messageId } : {}),
|
|
675
|
+
}, config.sessionStore);
|
|
676
|
+
const coveredTranscriptEntryCount = (prepared?.existingTranscriptEntryCount ?? 0) + 1;
|
|
245
677
|
const state = createSessionState({
|
|
246
678
|
runtimeAgent: config.runtimeAgent,
|
|
247
679
|
sessionId: config.sessionId,
|
|
@@ -255,344 +687,428 @@ export async function runUni(options) {
|
|
|
255
687
|
const abortController = new AbortController();
|
|
256
688
|
const timeout = setTimeout(() => abortController.abort(), config.timeoutMs);
|
|
257
689
|
const hookDispatcher = new UniHookDispatcher(options.runtimeHooks);
|
|
690
|
+
const turn = new UniTurnAccumulator({
|
|
691
|
+
runtimeAgent: config.runtimeAgent,
|
|
692
|
+
messageId: config.messageId,
|
|
693
|
+
});
|
|
694
|
+
const streamFilter = aligned ? new ArtifactStreamFilter() : undefined;
|
|
695
|
+
const artifactAccumulator = aligned
|
|
696
|
+
? new ArtifactAccumulator({ urlWhitelist: config.artifactsUrlWhitelist })
|
|
697
|
+
: undefined;
|
|
698
|
+
const workspaceDir = config.projectDir ?? config.dataDir;
|
|
699
|
+
const operationRecorder = aligned && config.channel !== "CRON"
|
|
700
|
+
? createArtifactOperationRecorder({
|
|
701
|
+
dataDir: config.dataDir,
|
|
702
|
+
logicalSessionId: config.sessionId,
|
|
703
|
+
storeName: config.sessionStore,
|
|
704
|
+
workspaceDir,
|
|
705
|
+
})
|
|
706
|
+
: undefined;
|
|
258
707
|
const activeToolCalls = new Map();
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
let
|
|
264
|
-
let
|
|
265
|
-
let
|
|
266
|
-
let
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const entry = pendingTranscriptEntries.shift();
|
|
279
|
-
if (!entry) {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
await appendUniTranscriptEntry(config.dataDir, sessionId, entry, config.sessionStore).catch((err) => { uniLogger.warn("failed to append buffered transcript", { error: String(err) }); });
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
const setPersistenceSessionId = async (sessionId) => {
|
|
286
|
-
if (!persistenceSessionId) {
|
|
287
|
-
persistenceSessionId = sessionId;
|
|
288
|
-
await ensureSessionPersistence(sessionId);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (persistenceSessionId !== sessionId) {
|
|
292
|
-
uniLogger.warn("provider session id differed from transcript session id", {
|
|
293
|
-
transcriptSessionId: persistenceSessionId,
|
|
294
|
-
providerSessionId: sessionId,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
708
|
+
const observedToolStartIds = new Set();
|
|
709
|
+
const recordedCompletedOnlyToolIds = new Set();
|
|
710
|
+
const observedProviderSessionIds = new Set(prepared?.providerSessionId ? [prepared.providerSessionId] : []);
|
|
711
|
+
let rawProviderText = "";
|
|
712
|
+
let observedDoneText;
|
|
713
|
+
let observedUsage;
|
|
714
|
+
let observedModel;
|
|
715
|
+
let observedError;
|
|
716
|
+
let observedTitle;
|
|
717
|
+
const emitProgress = async (event) => {
|
|
718
|
+
progressEvents.push(event);
|
|
719
|
+
await emitProgressEvent({
|
|
720
|
+
progressEvent: event,
|
|
721
|
+
state,
|
|
722
|
+
sink,
|
|
723
|
+
websocketSink,
|
|
724
|
+
onProgress: options.onProgress,
|
|
725
|
+
onExternalEvent: options.onExternalEvent,
|
|
726
|
+
});
|
|
297
727
|
};
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
pendingTranscriptEntries.push(entry);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
await ensureSessionPersistence(persistenceSessionId);
|
|
304
|
-
await appendUniTranscriptEntry(config.dataDir, persistenceSessionId, entry, config.sessionStore).catch((err) => { uniLogger.warn("failed to append transcript", { error: String(err) }); });
|
|
728
|
+
const appendTurnEntries = async (entries) => {
|
|
729
|
+
await appendUniTranscriptEntries(config.dataDir, config.sessionId, entries, config.sessionStore);
|
|
305
730
|
};
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
await ensureSessionPersistence(persistenceSessionId);
|
|
311
|
-
return persistenceSessionId;
|
|
731
|
+
const flushCompletedTools = async () => {
|
|
732
|
+
if (turn.hasCompletedToolBatch())
|
|
733
|
+
await appendTurnEntries(turn.flushToolBatch());
|
|
312
734
|
};
|
|
313
735
|
const createToolHookContext = () => ({
|
|
314
736
|
runtimeAgent: config.runtimeAgent,
|
|
315
|
-
sessionId:
|
|
316
|
-
messageId:
|
|
317
|
-
channel:
|
|
737
|
+
sessionId: config.sessionId,
|
|
738
|
+
messageId: config.messageId,
|
|
739
|
+
channel: config.channel,
|
|
318
740
|
projectDir: config.projectDir,
|
|
319
741
|
});
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
pendingTranscriptToolCallIds.add(toolCallId);
|
|
325
|
-
pendingTranscriptToolCalls.push({
|
|
326
|
-
id: toolCallId,
|
|
327
|
-
name: toolName,
|
|
328
|
-
arguments: params,
|
|
329
|
-
});
|
|
330
|
-
};
|
|
331
|
-
const flushPendingToolBatch = async () => {
|
|
332
|
-
if (pendingTranscriptToolCalls.length === 0) {
|
|
333
|
-
pendingTranscriptToolResults.length = 0;
|
|
334
|
-
pendingTranscriptToolResultIds.clear();
|
|
742
|
+
const emitVisibleText = async (type, text) => {
|
|
743
|
+
rawProviderText += text;
|
|
744
|
+
const visible = streamFilter ? streamFilter.push(text) : text;
|
|
745
|
+
if (!visible)
|
|
335
746
|
return;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
timestamp: new Date().toISOString(),
|
|
342
|
-
runtimeAgent: config.runtimeAgent,
|
|
343
|
-
messageId: state.messageId,
|
|
344
|
-
});
|
|
345
|
-
pendingAssistantContent = "";
|
|
346
|
-
pendingTranscriptToolCalls.length = 0;
|
|
347
|
-
pendingTranscriptToolCallIds.clear();
|
|
348
|
-
for (const entry of pendingTranscriptToolResults) {
|
|
349
|
-
await appendTranscriptEntry(entry);
|
|
350
|
-
}
|
|
351
|
-
pendingTranscriptToolResults.length = 0;
|
|
352
|
-
pendingTranscriptToolResultIds.clear();
|
|
353
|
-
};
|
|
354
|
-
const recordPendingTranscriptToolResult = (params) => {
|
|
355
|
-
if (pendingTranscriptToolResultIds.has(params.toolCallId)) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
pendingTranscriptToolResultIds.add(params.toolCallId);
|
|
359
|
-
pendingTranscriptToolResults.push({
|
|
360
|
-
role: "tool_result",
|
|
361
|
-
toolCallId: params.toolCallId,
|
|
362
|
-
toolName: params.toolName,
|
|
363
|
-
content: params.output,
|
|
364
|
-
isError: params.isError,
|
|
365
|
-
timestamp: new Date().toISOString(),
|
|
366
|
-
runtimeAgent: config.runtimeAgent,
|
|
367
|
-
messageId: state.messageId,
|
|
368
|
-
});
|
|
747
|
+
await flushCompletedTools();
|
|
748
|
+
turn.addText(visible);
|
|
749
|
+
const normalized = normalizeProviderEvent(config.sessionId, config.messageId, { type, text: visible });
|
|
750
|
+
for (const event of normalized)
|
|
751
|
+
await emitProgress(event);
|
|
369
752
|
};
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
const content = pendingAssistantContent || fallbackContent || "";
|
|
373
|
-
if (!content && !fallbackContent) {
|
|
753
|
+
const bindProviderSession = async (providerSessionId) => {
|
|
754
|
+
if (!aligned)
|
|
374
755
|
return;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
content,
|
|
379
|
-
timestamp: new Date().toISOString(),
|
|
756
|
+
await bindUniProviderSession({
|
|
757
|
+
dataDir: config.dataDir,
|
|
758
|
+
logicalSessionId: config.sessionId,
|
|
380
759
|
runtimeAgent: config.runtimeAgent,
|
|
381
|
-
|
|
382
|
-
|
|
760
|
+
providerSessionId,
|
|
761
|
+
storeName: config.sessionStore,
|
|
383
762
|
});
|
|
384
|
-
pendingAssistantContent = "";
|
|
385
763
|
};
|
|
386
764
|
try {
|
|
387
|
-
const promptText = config.input.kind === "text"
|
|
388
|
-
? config.input.message
|
|
389
|
-
: config.input.singleUserText ?? `Loaded ${config.input.messages.length} structured messages`;
|
|
390
|
-
metadataTitle = promptText.length > 80 ? promptText.slice(0, 77) + "..." : promptText;
|
|
391
765
|
uniLogger.info("uni run started", {
|
|
392
766
|
runtimeAgent: config.runtimeAgent,
|
|
393
|
-
sessionId:
|
|
394
|
-
messageId:
|
|
767
|
+
sessionId: config.sessionId,
|
|
768
|
+
messageId: config.messageId,
|
|
395
769
|
channel: config.channel,
|
|
396
770
|
sessionStore: config.sessionStore,
|
|
397
771
|
});
|
|
398
|
-
await
|
|
399
|
-
role: "user",
|
|
400
|
-
content: promptText,
|
|
401
|
-
timestamp: new Date().toISOString(),
|
|
402
|
-
runtimeAgent: config.runtimeAgent,
|
|
403
|
-
messageId: state.messageId,
|
|
404
|
-
});
|
|
405
|
-
const startEvent = {
|
|
772
|
+
await emitProgress({
|
|
406
773
|
type: "start",
|
|
407
|
-
sessionId: state.sessionId,
|
|
408
|
-
messageId: state.messageId,
|
|
409
|
-
message: promptText,
|
|
410
|
-
};
|
|
411
|
-
progressEvents.push(startEvent);
|
|
412
|
-
await emitProgressEvent({
|
|
413
|
-
progressEvent: startEvent,
|
|
414
|
-
state,
|
|
415
|
-
sink,
|
|
416
|
-
websocketSink,
|
|
417
|
-
onProgress: options.onProgress,
|
|
418
|
-
onExternalEvent: options.onExternalEvent,
|
|
419
|
-
});
|
|
420
|
-
const providerResult = await provider.run({
|
|
421
|
-
runtimeAgent: config.runtimeAgent,
|
|
422
|
-
dataDir: config.dataDir,
|
|
423
|
-
projectDir: config.projectDir,
|
|
424
774
|
sessionId: config.sessionId,
|
|
425
775
|
messageId: config.messageId,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
776
|
+
message: rawPrompt,
|
|
777
|
+
});
|
|
778
|
+
if (aligned && prepared?.isNew) {
|
|
779
|
+
await emitProgress({
|
|
780
|
+
type: "session_reset",
|
|
781
|
+
sessionId: config.sessionId,
|
|
782
|
+
messageId: config.messageId,
|
|
783
|
+
action: "new",
|
|
784
|
+
message: "Logical session started",
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
let providerResult;
|
|
788
|
+
try {
|
|
789
|
+
providerResult = await provider.run({
|
|
790
|
+
runtimeAgent: config.runtimeAgent,
|
|
791
|
+
logicalSessionId: config.sessionId,
|
|
792
|
+
dataDir: config.dataDir,
|
|
793
|
+
projectDir: config.projectDir,
|
|
794
|
+
providerSessionId: aligned ? prepared?.providerSessionId : nonEmpty(options.sessionId),
|
|
795
|
+
sessionId: aligned ? prepared?.providerSessionId : nonEmpty(options.sessionId),
|
|
796
|
+
messageId: config.messageId,
|
|
797
|
+
channel: config.channel,
|
|
798
|
+
baseUrl: config.baseUrl,
|
|
799
|
+
apiKey: config.apiKey,
|
|
800
|
+
authToken: config.authToken,
|
|
801
|
+
model: config.model,
|
|
802
|
+
contextWindow: config.contextWindow,
|
|
803
|
+
flashModel: config.flashModel,
|
|
804
|
+
timeoutMs: config.timeoutMs,
|
|
805
|
+
runtimeConfig: config.runtimeConfig,
|
|
806
|
+
input: providerInput(config.input, aligned),
|
|
807
|
+
signal: abortController.signal,
|
|
808
|
+
}, async (providerEvent) => {
|
|
809
|
+
let eventForProgress = providerEvent;
|
|
810
|
+
if (providerEvent.type === "session_started") {
|
|
811
|
+
observedModel = providerEvent.model ?? observedModel;
|
|
812
|
+
if (aligned) {
|
|
813
|
+
if (!observedProviderSessionIds.has(providerEvent.sessionId)) {
|
|
814
|
+
await bindProviderSession(providerEvent.sessionId);
|
|
815
|
+
observedProviderSessionIds.add(providerEvent.sessionId);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
for (const event of normalizeProviderEvent(config.sessionId, config.messageId, providerEvent)) {
|
|
820
|
+
await emitProgress(event);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
441
823
|
return;
|
|
442
824
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
825
|
+
if (aligned) {
|
|
826
|
+
providerEvent = redactProviderEventForPublicBoundary(providerEvent, observedProviderSessionIds);
|
|
827
|
+
eventForProgress = providerEvent;
|
|
828
|
+
}
|
|
829
|
+
if (providerEvent.type === "text_delta" || providerEvent.type === "message") {
|
|
830
|
+
await emitVisibleText(providerEvent.type, providerEvent.text);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (providerEvent.type === "thinking_delta") {
|
|
834
|
+
if (aligned) {
|
|
835
|
+
await flushCompletedTools();
|
|
836
|
+
turn.addThinking(providerEvent.text);
|
|
837
|
+
}
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (providerEvent.type === "file_operation") {
|
|
841
|
+
const auditEvent = artifactAuditFileOperation(providerEvent);
|
|
842
|
+
await Promise.resolve(operationRecorder?.recordObservedFileOperation(auditEvent))
|
|
843
|
+
.catch(() => undefined);
|
|
844
|
+
if (!providerEvent.isError && artifactAccumulator) {
|
|
845
|
+
artifactAccumulator.addInferred(inferArtifactsFromToolExecution({
|
|
846
|
+
toolName: canonicalToolName(providerEvent.name),
|
|
847
|
+
input: toRecord(providerEvent.input),
|
|
848
|
+
result: inferenceResultForTool(providerEvent),
|
|
849
|
+
isError: false,
|
|
850
|
+
workspaceDir,
|
|
851
|
+
}).filter((artifact) => !isInternalArtifact(artifact, config.dataDir)));
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (providerEvent.type === "tool_start") {
|
|
856
|
+
const params = toRecord(providerEvent.input);
|
|
857
|
+
observedToolStartIds.add(providerEvent.toolCallId);
|
|
858
|
+
activeToolCalls.set(providerEvent.toolCallId, {
|
|
859
|
+
toolName: providerEvent.name,
|
|
455
860
|
params,
|
|
456
861
|
startedAt: Date.now(),
|
|
457
862
|
});
|
|
458
|
-
|
|
863
|
+
turn.startTool(providerEvent);
|
|
864
|
+
await Promise.resolve(operationRecorder?.onToolStart(artifactAuditStartEvent(providerEvent)))
|
|
865
|
+
.catch(() => undefined);
|
|
459
866
|
await hookDispatcher.dispatchBeforeToolCall({
|
|
460
|
-
toolCallId:
|
|
461
|
-
toolName:
|
|
867
|
+
toolCallId: providerEvent.toolCallId,
|
|
868
|
+
toolName: providerEvent.name,
|
|
462
869
|
params,
|
|
463
870
|
}, createToolHookContext());
|
|
464
871
|
}
|
|
465
|
-
if (
|
|
466
|
-
const active = activeToolCalls.get(
|
|
467
|
-
const params = active?.params
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
872
|
+
else if (providerEvent.type === "tool_end") {
|
|
873
|
+
const active = activeToolCalls.get(providerEvent.toolCallId);
|
|
874
|
+
const params = active?.params ?? toRecord(providerEvent.input);
|
|
875
|
+
const toolEndEvent = active
|
|
876
|
+
? {
|
|
877
|
+
...providerEvent,
|
|
878
|
+
name: active.toolName,
|
|
879
|
+
input: active.params,
|
|
880
|
+
}
|
|
881
|
+
: providerEvent;
|
|
882
|
+
eventForProgress = toolEndEvent;
|
|
883
|
+
const paired = turn.endTool(toolEndEvent);
|
|
884
|
+
await Promise.resolve(operationRecorder?.onToolEnd(artifactAuditEndEvent(toolEndEvent)))
|
|
885
|
+
.catch(() => undefined);
|
|
886
|
+
if (config.runtimeAgent === "opencode"
|
|
887
|
+
&& !observedToolStartIds.has(toolEndEvent.toolCallId)
|
|
888
|
+
&& !recordedCompletedOnlyToolIds.has(toolEndEvent.toolCallId)
|
|
889
|
+
&& isTrustedOpenCodeCompletedOnlyFileTool(toolEndEvent)) {
|
|
890
|
+
recordedCompletedOnlyToolIds.add(toolEndEvent.toolCallId);
|
|
891
|
+
await Promise.resolve(operationRecorder?.recordObservedFileOperation({
|
|
892
|
+
operationId: toolEndEvent.toolCallId,
|
|
893
|
+
name: toolEndEvent.name,
|
|
894
|
+
input: toolEndEvent.input,
|
|
895
|
+
output: toolEndEvent.output,
|
|
896
|
+
isError: toolEndEvent.isError,
|
|
897
|
+
})).catch(() => undefined);
|
|
898
|
+
}
|
|
899
|
+
await contextStore?.persistToolResult({
|
|
900
|
+
toolCallId: toolEndEvent.toolCallId,
|
|
901
|
+
toolName: toolEndEvent.name,
|
|
902
|
+
content: toolEndEvent.output,
|
|
903
|
+
}).catch((error) => {
|
|
904
|
+
uniLogger.warn("failed to persist observable tool result", { error: String(error) });
|
|
477
905
|
});
|
|
906
|
+
if (aligned && paired && !providerEvent.isError && artifactAccumulator) {
|
|
907
|
+
artifactAccumulator.addChild(extractChildArtifacts(toolEndEvent.name, toolEndEvent.output, workspaceDir)
|
|
908
|
+
.filter((artifact) => !isInternalArtifact(artifact, config.dataDir)));
|
|
909
|
+
artifactAccumulator.addInferred(inferArtifactsFromToolExecution({
|
|
910
|
+
toolName: canonicalToolName(toolEndEvent.name),
|
|
911
|
+
input: params,
|
|
912
|
+
result: inferenceResult(toolEndEvent),
|
|
913
|
+
isError: toolEndEvent.isError,
|
|
914
|
+
workspaceDir,
|
|
915
|
+
}).filter((artifact) => !isInternalArtifact(artifact, config.dataDir)));
|
|
916
|
+
}
|
|
478
917
|
await hookDispatcher.dispatchAfterToolCall({
|
|
479
|
-
toolCallId:
|
|
480
|
-
toolName:
|
|
918
|
+
toolCallId: providerEvent.toolCallId,
|
|
919
|
+
toolName: toolEndEvent.name,
|
|
481
920
|
params,
|
|
482
|
-
result:
|
|
483
|
-
error:
|
|
921
|
+
result: toolEndEvent.output,
|
|
922
|
+
error: toolEndEvent.isError ? toolEndEvent.output : undefined,
|
|
484
923
|
durationMs: active ? Date.now() - active.startedAt : undefined,
|
|
485
924
|
}, createToolHookContext());
|
|
486
|
-
activeToolCalls.delete(
|
|
925
|
+
activeToolCalls.delete(providerEvent.toolCallId);
|
|
487
926
|
}
|
|
488
|
-
if (
|
|
489
|
-
await
|
|
490
|
-
|
|
927
|
+
else if (providerEvent.type === "step_finished") {
|
|
928
|
+
await flushCompletedTools();
|
|
929
|
+
return;
|
|
491
930
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
931
|
+
else if (providerEvent.type === "done") {
|
|
932
|
+
observedDoneText = providerEvent.text ?? observedDoneText;
|
|
933
|
+
observedUsage = accumulateUsage(observedUsage, providerEvent.usage);
|
|
934
|
+
observedModel = providerEvent.model ?? observedModel;
|
|
935
|
+
await flushCompletedTools();
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
else if (providerEvent.type === "error") {
|
|
939
|
+
observedError = providerEvent.message;
|
|
940
|
+
observedUsage = accumulateUsage(observedUsage, providerEvent.usage);
|
|
941
|
+
observedModel = providerEvent.model ?? observedModel;
|
|
942
|
+
}
|
|
943
|
+
else if (providerEvent.type === "title") {
|
|
944
|
+
observedTitle = providerEvent.title;
|
|
945
|
+
}
|
|
946
|
+
for (const event of normalizeProviderEvent(config.sessionId, config.messageId, eventForProgress)) {
|
|
947
|
+
await emitProgress(event);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
if (error instanceof UniSessionError)
|
|
953
|
+
throw error;
|
|
954
|
+
providerResult = { error: error instanceof Error ? error.message : String(error) };
|
|
955
|
+
}
|
|
503
956
|
if (providerResult.sessionId) {
|
|
504
|
-
await
|
|
505
|
-
|
|
957
|
+
await bindProviderSession(providerResult.sessionId);
|
|
958
|
+
observedProviderSessionIds.add(providerResult.sessionId);
|
|
959
|
+
}
|
|
960
|
+
const filterTail = streamFilter?.finish() ?? "";
|
|
961
|
+
if (filterTail) {
|
|
962
|
+
await flushCompletedTools();
|
|
963
|
+
turn.addText(filterTail);
|
|
964
|
+
for (const event of normalizeProviderEvent(config.sessionId, config.messageId, {
|
|
965
|
+
type: "text_delta",
|
|
966
|
+
text: filterTail,
|
|
967
|
+
}))
|
|
968
|
+
await emitProgress(event);
|
|
969
|
+
}
|
|
970
|
+
const hasExplicitTerminalText = providerResult.text !== undefined || observedDoneText !== undefined;
|
|
971
|
+
const rawProviderFinalText = providerResult.text ?? observedDoneText ?? rawProviderText;
|
|
972
|
+
const providerFinalText = aligned
|
|
973
|
+
? redactKnownProviderIds(rawProviderFinalText, observedProviderSessionIds)
|
|
974
|
+
: rawProviderFinalText;
|
|
975
|
+
const parsedArtifacts = aligned
|
|
976
|
+
? parseAndStripArtifacts(providerFinalText, { workspaceDir })
|
|
977
|
+
: { cleanedText: providerFinalText, artifacts: [], warnings: [] };
|
|
978
|
+
const streamedArtifactParse = aligned
|
|
979
|
+
&& rawProviderText !== providerFinalText
|
|
980
|
+
&& rawProviderText.includes(ARTIFACTS_START_TAG)
|
|
981
|
+
? parseAndStripArtifacts(rawProviderText, { workspaceDir })
|
|
982
|
+
: undefined;
|
|
983
|
+
artifactAccumulator?.addOwn(parsedArtifacts.artifacts.filter((artifact) => !isInternalArtifact(artifact, config.dataDir)));
|
|
984
|
+
artifactAccumulator?.addOwn((streamedArtifactParse?.artifacts ?? [])
|
|
985
|
+
.filter((artifact) => !isInternalArtifact(artifact, config.dataDir)));
|
|
986
|
+
for (const warning of new Set([
|
|
987
|
+
...parsedArtifacts.warnings,
|
|
988
|
+
...(streamedArtifactParse?.warnings ?? []),
|
|
989
|
+
])) {
|
|
990
|
+
await emitProgress({
|
|
991
|
+
type: "diagnostic",
|
|
992
|
+
sessionId: config.sessionId,
|
|
993
|
+
messageId: config.messageId,
|
|
994
|
+
level: "warn",
|
|
995
|
+
scope: "runner",
|
|
996
|
+
phase: "artifact_declaration",
|
|
997
|
+
message: warning,
|
|
998
|
+
});
|
|
506
999
|
}
|
|
507
|
-
if (
|
|
508
|
-
|
|
1000
|
+
if (rawProviderText && state.finalText !== parsedArtifacts.cleanedText) {
|
|
1001
|
+
await emitProgress({
|
|
1002
|
+
type: "diagnostic",
|
|
1003
|
+
sessionId: config.sessionId,
|
|
1004
|
+
messageId: config.messageId,
|
|
1005
|
+
level: "warn",
|
|
1006
|
+
scope: "runner",
|
|
1007
|
+
phase: "provider_text_mismatch",
|
|
1008
|
+
message: "Provider terminal text differed from streamed text; the terminal result is authoritative",
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
const usage = providerResult.usage ?? observedUsage;
|
|
1012
|
+
const rawError = providerResult.error ?? observedError;
|
|
1013
|
+
const error = rawError
|
|
1014
|
+
? redactKnownProviderIds(rawError, observedProviderSessionIds)
|
|
1015
|
+
: undefined;
|
|
1016
|
+
const durationMs = Date.now() - startedAt;
|
|
1017
|
+
const artifacts = artifactAccumulator?.dump() ?? [];
|
|
1018
|
+
state.finalText = parsedArtifacts.cleanedText;
|
|
1019
|
+
if (!usage && aligned) {
|
|
1020
|
+
await emitProgress({
|
|
1021
|
+
type: "diagnostic",
|
|
1022
|
+
sessionId: config.sessionId,
|
|
1023
|
+
messageId: config.messageId,
|
|
1024
|
+
level: "warn",
|
|
1025
|
+
scope: "runner",
|
|
1026
|
+
phase: "provider_usage_unavailable",
|
|
1027
|
+
message: `${config.runtimeAgent} did not expose token usage; transport compatibility uses zero without persisting it`,
|
|
1028
|
+
});
|
|
509
1029
|
}
|
|
510
|
-
|
|
511
|
-
|
|
1030
|
+
const finalEntries = turn.finalize({
|
|
1031
|
+
fallbackContent: parsedArtifacts.cleanedText,
|
|
1032
|
+
forceFallbackContent: hasExplicitTerminalText,
|
|
1033
|
+
usage,
|
|
1034
|
+
durationMs,
|
|
1035
|
+
error,
|
|
1036
|
+
artifacts: artifacts.length > 0 ? artifacts : undefined,
|
|
1037
|
+
});
|
|
1038
|
+
await appendTurnEntries(finalEntries);
|
|
1039
|
+
if (usage && contextStore) {
|
|
1040
|
+
const rawModel = providerResult.model
|
|
1041
|
+
?? observedModel
|
|
1042
|
+
?? configuredProviderModel(config)
|
|
1043
|
+
?? `runtime:${config.runtimeAgent}`;
|
|
1044
|
+
await contextStore.recordModelUsage({
|
|
1045
|
+
model: aligned
|
|
1046
|
+
? redactKnownProviderIds(rawModel, observedProviderSessionIds)
|
|
1047
|
+
: rawModel,
|
|
1048
|
+
usage,
|
|
1049
|
+
coveredTranscriptEntryCount,
|
|
1050
|
+
}).catch((contextError) => {
|
|
1051
|
+
uniLogger.warn("failed to update model usage context", { error: String(contextError) });
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const rawFinalTitle = providerResult.title ?? observedTitle;
|
|
1055
|
+
const finalTitle = rawFinalTitle && aligned
|
|
1056
|
+
? redactKnownProviderIds(rawFinalTitle, observedProviderSessionIds)
|
|
1057
|
+
: rawFinalTitle;
|
|
1058
|
+
await updateUniSessionMetadata(config.dataDir, config.sessionId, {
|
|
1059
|
+
...(finalTitle ? { title: finalTitle } : {}),
|
|
1060
|
+
}, config.sessionStore);
|
|
1061
|
+
if (finalTitle) {
|
|
1062
|
+
await emitProgress({
|
|
512
1063
|
type: "title_updated",
|
|
513
|
-
sessionId:
|
|
514
|
-
messageId:
|
|
515
|
-
title:
|
|
516
|
-
};
|
|
517
|
-
progressEvents.push(event);
|
|
518
|
-
await emitProgressEvent({
|
|
519
|
-
progressEvent: event,
|
|
520
|
-
state,
|
|
521
|
-
sink,
|
|
522
|
-
websocketSink,
|
|
523
|
-
onProgress: options.onProgress,
|
|
524
|
-
onExternalEvent: options.onExternalEvent,
|
|
1064
|
+
sessionId: config.sessionId,
|
|
1065
|
+
messageId: config.messageId,
|
|
1066
|
+
title: finalTitle,
|
|
525
1067
|
});
|
|
526
1068
|
}
|
|
527
|
-
|
|
1069
|
+
const resultPayload = {
|
|
1070
|
+
text: parsedArtifacts.cleanedText,
|
|
1071
|
+
usage: usage ?? { ...ZERO_USAGE },
|
|
1072
|
+
durationMs,
|
|
1073
|
+
...(error ? { error } : {}),
|
|
1074
|
+
...(aligned && artifacts.length > 0 ? { artifacts } : {}),
|
|
1075
|
+
};
|
|
1076
|
+
if (error) {
|
|
528
1077
|
uniLogger.error("uni run failed", {
|
|
529
1078
|
runtimeAgent: config.runtimeAgent,
|
|
530
|
-
sessionId:
|
|
531
|
-
durationMs
|
|
532
|
-
error
|
|
533
|
-
});
|
|
534
|
-
// Persist assistant error transcript entry
|
|
535
|
-
await ensureFinalPersistenceSession();
|
|
536
|
-
await flushPendingToolBatch();
|
|
537
|
-
await appendTranscriptEntry({
|
|
538
|
-
role: "assistant",
|
|
539
|
-
content: state.finalText,
|
|
540
|
-
timestamp: new Date().toISOString(),
|
|
541
|
-
runtimeAgent: config.runtimeAgent,
|
|
542
|
-
messageId: state.messageId,
|
|
543
|
-
usage: providerResult.usage,
|
|
544
|
-
durationMs: Date.now() - startedAt,
|
|
545
|
-
error: providerResult.error,
|
|
1079
|
+
sessionId: config.sessionId,
|
|
1080
|
+
durationMs,
|
|
1081
|
+
error,
|
|
546
1082
|
});
|
|
547
1083
|
await emitExternalEvent(sink, {
|
|
548
1084
|
type: "error",
|
|
549
|
-
sessionId:
|
|
550
|
-
messageId:
|
|
551
|
-
channel:
|
|
552
|
-
user:
|
|
553
|
-
message:
|
|
1085
|
+
sessionId: config.sessionId,
|
|
1086
|
+
messageId: config.messageId,
|
|
1087
|
+
channel: config.channel,
|
|
1088
|
+
user: config.user,
|
|
1089
|
+
message: error,
|
|
554
1090
|
}, state, options.onExternalEvent);
|
|
555
|
-
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
uniLogger.info("uni run completed", {
|
|
556
1094
|
runtimeAgent: config.runtimeAgent,
|
|
557
|
-
sessionId:
|
|
558
|
-
|
|
559
|
-
usage
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
1095
|
+
sessionId: config.sessionId,
|
|
1096
|
+
durationMs,
|
|
1097
|
+
...(usage ? { inputTokens: usage.input, outputTokens: usage.output } : {}),
|
|
1098
|
+
});
|
|
1099
|
+
await emitExternalEvent(sink, {
|
|
1100
|
+
type: "done",
|
|
1101
|
+
sessionId: config.sessionId,
|
|
1102
|
+
messageId: config.messageId,
|
|
1103
|
+
channel: config.channel,
|
|
1104
|
+
user: config.user,
|
|
1105
|
+
result: resultPayload,
|
|
1106
|
+
}, state, options.onExternalEvent);
|
|
565
1107
|
}
|
|
566
|
-
const result = {
|
|
567
|
-
text: state.finalText,
|
|
568
|
-
usage: providerResult.usage ?? { input: 0, output: 0, total: 0 },
|
|
569
|
-
durationMs: Date.now() - startedAt,
|
|
570
|
-
};
|
|
571
|
-
uniLogger.info("uni run completed", {
|
|
572
|
-
runtimeAgent: config.runtimeAgent,
|
|
573
|
-
sessionId: state.sessionId,
|
|
574
|
-
durationMs: result.durationMs,
|
|
575
|
-
inputTokens: result.usage.input,
|
|
576
|
-
outputTokens: result.usage.output,
|
|
577
|
-
});
|
|
578
|
-
// Persist assistant transcript entry
|
|
579
|
-
await ensureFinalPersistenceSession();
|
|
580
|
-
await appendPendingAssistantContentEntry(state.finalText, {
|
|
581
|
-
usage: result.usage,
|
|
582
|
-
durationMs: result.durationMs,
|
|
583
|
-
});
|
|
584
|
-
await emitExternalEvent(sink, {
|
|
585
|
-
type: "done",
|
|
586
|
-
sessionId: state.sessionId,
|
|
587
|
-
messageId: state.messageId,
|
|
588
|
-
channel: state.channel,
|
|
589
|
-
user: state.user,
|
|
590
|
-
result,
|
|
591
|
-
}, state, options.onExternalEvent);
|
|
592
1108
|
return {
|
|
593
1109
|
runtimeAgent: config.runtimeAgent,
|
|
594
|
-
sessionId:
|
|
595
|
-
...
|
|
1110
|
+
sessionId: config.sessionId,
|
|
1111
|
+
...resultPayload,
|
|
596
1112
|
progressEvents,
|
|
597
1113
|
externalEvents: state.externalEvents,
|
|
598
1114
|
};
|