@nuvin/session 0.4.0-rc.1 → 0.4.0-rc.11
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/dist/chunk-4AOFJGGN.js +141 -0
- package/dist/{chunk-DTLHDYZC.js → chunk-4FXP5TSI.js} +249 -46
- package/dist/{chunk-X6DYNSV2.js → chunk-O3YF6Q77.js} +50 -1
- package/dist/client/control-center-socket.d.ts +14 -0
- package/dist/client/control-center-socket.d.ts.map +1 -0
- package/dist/client/daemon-client.d.ts.map +1 -1
- package/dist/client/data-client.d.ts +8 -1
- package/dist/client/data-client.d.ts.map +1 -1
- package/dist/client/http-data-client.d.ts.map +1 -1
- package/dist/client/index.js +270 -8
- package/dist/client/session-client.d.ts +8 -2
- package/dist/client/session-client.d.ts.map +1 -1
- package/dist/client/websocket.d.ts +1 -1
- package/dist/client/websocket.d.ts.map +1 -1
- package/dist/controller/agent-channel.d.ts +7 -2
- package/dist/controller/agent-channel.d.ts.map +1 -1
- package/dist/controller/index.js +469 -82
- package/dist/controller/session-controller.d.ts +37 -9
- package/dist/controller/session-controller.d.ts.map +1 -1
- package/dist/protocol/background-work.d.ts +3 -1
- package/dist/protocol/background-work.d.ts.map +1 -1
- package/dist/protocol/control-center.d.ts +141 -0
- package/dist/protocol/control-center.d.ts.map +1 -0
- package/dist/protocol/data-rpc.d.ts +13 -1
- package/dist/protocol/data-rpc.d.ts.map +1 -1
- package/dist/protocol/index.d.ts +2 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +14 -2
- package/dist/protocol/submission-recovery.d.ts +6 -0
- package/dist/protocol/submission-recovery.d.ts.map +1 -0
- package/dist/protocol/types.d.ts +159 -17
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/state/approvals.d.ts.map +1 -1
- package/dist/state/index.js +11 -3
- package/dist/state/messages.d.ts +30 -2
- package/dist/state/messages.d.ts.map +1 -1
- package/dist/state/session.d.ts +4 -1
- package/dist/state/session.d.ts.map +1 -1
- package/dist/state/tool-preview.d.ts.map +1 -1
- package/dist/state/tool-preview.js +1 -45
- package/dist/test-utils/index.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-5PHEC5GZ.js +0 -85
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/protocol/control-center.ts
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function isStringArray(value) {
|
|
6
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
7
|
+
}
|
|
8
|
+
function isScope(value) {
|
|
9
|
+
return isRecord(value) && ["profile", "workspace", "cwd"].every(
|
|
10
|
+
(key) => value[key] === void 0 || typeof value[key] === "string"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function isAgentDefinition(value) {
|
|
14
|
+
return isRecord(value) && ["filename", "name", "description", "systemPrompt"].every(
|
|
15
|
+
(key) => typeof value[key] === "string"
|
|
16
|
+
) && (value.model === void 0 || typeof value.model === "string") && typeof value.enabled === "boolean" && isStringArray(value.tools) && isStringArray(value.skills);
|
|
17
|
+
}
|
|
18
|
+
function isAgentMutation(value) {
|
|
19
|
+
return isRecord(value) && ["sourceKey", "agentId", "expectedRevision"].every((key) => typeof value[key] === "string");
|
|
20
|
+
}
|
|
21
|
+
var noArgs = (args) => args.length === 0;
|
|
22
|
+
var oneString = (args) => args.length === 1 && typeof args[0] === "string";
|
|
23
|
+
var argumentValidators = {
|
|
24
|
+
getState: noArgs,
|
|
25
|
+
completeSlashCommand: (args) => args.length === 2 && args.every((value) => typeof value === "string"),
|
|
26
|
+
runSlashCommand: oneString,
|
|
27
|
+
setConfig: (args) => args.length === 1 && Array.isArray(args[0]) && args[0].every(
|
|
28
|
+
(patch) => isRecord(patch) && typeof patch.key === "string" && Object.hasOwn(patch, "value") && patch.value !== void 0 && (patch.scope === "global" || patch.scope === "local")
|
|
29
|
+
),
|
|
30
|
+
listSkills: noArgs,
|
|
31
|
+
getSkill: oneString,
|
|
32
|
+
listAgents: noArgs,
|
|
33
|
+
getAgent: oneString,
|
|
34
|
+
getAgentEditorData: (args) => args.length === 0 || args.length === 1 && (args[0] === void 0 || typeof args[0] === "string"),
|
|
35
|
+
createAgent: (args) => args.length === 1 && isAgentDefinition(args[0]),
|
|
36
|
+
updateAgent: (args) => args.length === 1 && isAgentMutation(args[0]) && isAgentDefinition(args[0].definition),
|
|
37
|
+
deleteAgent: (args) => args.length === 1 && isAgentMutation(args[0]),
|
|
38
|
+
getCommand: oneString,
|
|
39
|
+
listMcpServers: noArgs,
|
|
40
|
+
mcpAction: (args) => args.length === 2 && ["enable", "disable", "reload", "login"].includes(args[0]) && typeof args[1] === "string",
|
|
41
|
+
listWorkspaceMemories: noArgs,
|
|
42
|
+
getWorkspaceMemory: oneString,
|
|
43
|
+
removeWorkspaceMemory: oneString,
|
|
44
|
+
listConsolidateProposals: noArgs,
|
|
45
|
+
getConsolidateProposal: oneString,
|
|
46
|
+
decideConsolidateProposal: (args) => (args.length === 2 || args.length === 3) && typeof args[0] === "string" && (args[1] === "approve" || args[1] === "reject") && (args.length === 2 || args[2] === void 0 || isStringArray(args[2])),
|
|
47
|
+
runConsolidate: noArgs
|
|
48
|
+
};
|
|
49
|
+
function parseControlCenterRequest(value) {
|
|
50
|
+
if (!isRecord(value) || !isScope(value.scope) || typeof value.operation !== "string" || !Object.hasOwn(argumentValidators, value.operation) || !Array.isArray(value.args) || !argumentValidators[value.operation](value.args)) {
|
|
51
|
+
throw new Error("Invalid management request.");
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/protocol/history-text.ts
|
|
57
|
+
function stripTerminalStringSequences(value) {
|
|
58
|
+
let sanitized = "";
|
|
59
|
+
for (let index = 0; index < value.length; ) {
|
|
60
|
+
const code = value.charCodeAt(index);
|
|
61
|
+
const next = value.charCodeAt(index + 1);
|
|
62
|
+
const isSevenBitSequence = code === 27 && [80, 88, 93, 94, 95].includes(next);
|
|
63
|
+
const isEightBitSequence = [144, 152, 157, 158, 159].includes(code);
|
|
64
|
+
if (!isSevenBitSequence && !isEightBitSequence) {
|
|
65
|
+
sanitized += value[index];
|
|
66
|
+
index += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
index += isSevenBitSequence ? 2 : 1;
|
|
70
|
+
while (index < value.length) {
|
|
71
|
+
const terminator = value.charCodeAt(index);
|
|
72
|
+
if (terminator === 7 || terminator === 156) {
|
|
73
|
+
index += 1;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (terminator === 27 && value.charCodeAt(index + 1) === 92) {
|
|
77
|
+
index += 2;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
index += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return sanitized;
|
|
84
|
+
}
|
|
85
|
+
function stripTerminalControlSequences(value) {
|
|
86
|
+
let sanitized = "";
|
|
87
|
+
for (let index = 0; index < value.length; ) {
|
|
88
|
+
const code = value.charCodeAt(index);
|
|
89
|
+
const isCsi = code === 155 || code === 27 && value.charCodeAt(index + 1) === 91;
|
|
90
|
+
if (isCsi) {
|
|
91
|
+
index += code === 27 ? 2 : 1;
|
|
92
|
+
while (index < value.length) {
|
|
93
|
+
const current = value.charCodeAt(index);
|
|
94
|
+
index += 1;
|
|
95
|
+
if (current >= 64 && current <= 126) break;
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (code === 27) {
|
|
100
|
+
index += 1;
|
|
101
|
+
while (index < value.length) {
|
|
102
|
+
const current = value.charCodeAt(index);
|
|
103
|
+
index += 1;
|
|
104
|
+
if (current < 32 || current > 47) break;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
sanitized += value[index];
|
|
109
|
+
index += 1;
|
|
110
|
+
}
|
|
111
|
+
return sanitized;
|
|
112
|
+
}
|
|
113
|
+
function sanitizeHistoryDisplayText(value) {
|
|
114
|
+
return stripTerminalControlSequences(stripTerminalStringSequences(value.normalize("NFKC"))).replace(new RegExp("\\p{Cc}", "gu"), (control) => /\s/u.test(control) ? " " : "").replace(/\s+/gu, " ").trim();
|
|
115
|
+
}
|
|
116
|
+
function normalizeHistoryText(value) {
|
|
117
|
+
return sanitizeHistoryDisplayText(value).toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
function escapeLikeLiteral(value) {
|
|
120
|
+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/protocol/model-identity.ts
|
|
124
|
+
function modelNameFromIdentity(modelIdentity) {
|
|
125
|
+
const separator = modelIdentity.indexOf(":");
|
|
126
|
+
return separator === -1 ? modelIdentity : modelIdentity.slice(separator + 1);
|
|
127
|
+
}
|
|
128
|
+
function providerFromIdentity(modelIdentity) {
|
|
129
|
+
const separator = modelIdentity.indexOf(":");
|
|
130
|
+
if (separator <= 0) return void 0;
|
|
131
|
+
return modelIdentity.slice(0, separator);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
parseControlCenterRequest,
|
|
136
|
+
sanitizeHistoryDisplayText,
|
|
137
|
+
normalizeHistoryText,
|
|
138
|
+
escapeLikeLiteral,
|
|
139
|
+
modelNameFromIdentity,
|
|
140
|
+
providerFromIdentity
|
|
141
|
+
};
|
|
@@ -3,8 +3,6 @@ var BACKGROUNDLESS_TOOLS = /* @__PURE__ */ new Set([
|
|
|
3
3
|
"FileRead",
|
|
4
4
|
"ViewFile",
|
|
5
5
|
"Ls",
|
|
6
|
-
"Grep",
|
|
7
|
-
"Glob",
|
|
8
6
|
"LoadSkill",
|
|
9
7
|
"Lsp",
|
|
10
8
|
"WebFetch",
|
|
@@ -179,6 +177,16 @@ function withUpdatedMessage(state, id, update) {
|
|
|
179
177
|
messages: state.messages.map((message) => message.id === id ? update(message) : message)
|
|
180
178
|
};
|
|
181
179
|
}
|
|
180
|
+
function withDelegatedModel(message, model) {
|
|
181
|
+
if (message.toolName !== "AssignTask") return message;
|
|
182
|
+
const prior = message.delegatedModel;
|
|
183
|
+
if (prior !== void 0) {
|
|
184
|
+
if (prior.runId !== model.runId || prior.turnNumber > model.turnNumber) return message;
|
|
185
|
+
if (prior.turnNumber === model.turnNumber && (prior.source === "resolution" || model.source === "assistant"))
|
|
186
|
+
return message;
|
|
187
|
+
}
|
|
188
|
+
return { ...message, delegatedModel: { ...model } };
|
|
189
|
+
}
|
|
182
190
|
function addMessage(state, message) {
|
|
183
191
|
return {
|
|
184
192
|
...state,
|
|
@@ -192,20 +200,33 @@ function cloneToolCallPath(toolCallPath) {
|
|
|
192
200
|
function scopedRegistryKey(toolCallPath, id) {
|
|
193
201
|
return scopedToolKey(toolCallPath, id);
|
|
194
202
|
}
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
function retryReasonText(reason, status) {
|
|
204
|
+
if (reason === "transport_error") return "Network interrupted";
|
|
205
|
+
if (reason === "stream_error") return "Provider stream interrupted";
|
|
206
|
+
return status === void 0 ? "Provider unavailable" : `Provider unavailable (HTTP ${status})`;
|
|
207
|
+
}
|
|
208
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
209
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
210
|
+
}
|
|
211
|
+
function formatProviderRetrySummary(detail) {
|
|
212
|
+
const last = detail.attempts[detail.attempts.length - 1];
|
|
213
|
+
if (last === void 0) return "Provider request interrupted";
|
|
214
|
+
const reason = retryReasonText(last.reason, last.status);
|
|
215
|
+
if (detail.outcome === "recovered") {
|
|
216
|
+
const restored = `restored after ${pluralize(detail.attempts.length, "retry", "retries")}`;
|
|
217
|
+
if (detail.settledAt === void 0) return `${reason} \u2014 ${restored}`;
|
|
218
|
+
const elapsedSeconds = Math.max(
|
|
219
|
+
0,
|
|
220
|
+
Math.round((detail.settledAt - detail.attempts[0].occurredAt) / 1e3)
|
|
221
|
+
);
|
|
222
|
+
return `${reason} \u2014 ${restored} \xB7 ${elapsedSeconds}s`;
|
|
223
|
+
}
|
|
224
|
+
if (detail.outcome === "stopped") {
|
|
225
|
+
return `${reason} \u2014 gave up after ${pluralize(detail.attempts.length, "attempt")}`;
|
|
226
|
+
}
|
|
227
|
+
const nextAttempt = Math.min(last.failedAttempt + 1, last.maxAttempts);
|
|
228
|
+
const delay = last.delayMs === 0 ? "now" : `in ${Math.max(1, Math.ceil(last.delayMs / 1e3))}s`;
|
|
229
|
+
return `${reason} \u2014 retrying attempt ${nextAttempt}/${last.maxAttempts} ${delay}`;
|
|
209
230
|
}
|
|
210
231
|
function removeProviderStreamMessages(state, assistantMessageId, reasoningMessageId, toolCallPath) {
|
|
211
232
|
const assistantKey = scopedRegistryKey(toolCallPath, assistantMessageId);
|
|
@@ -249,33 +270,39 @@ function upsertProviderRetryMessage(state, event, toolCallPath, now) {
|
|
|
249
270
|
...event.status !== void 0 ? { status: event.status } : {},
|
|
250
271
|
occurredAt: now
|
|
251
272
|
};
|
|
252
|
-
const text = formatProviderRetryText(event);
|
|
253
273
|
if (existingId) {
|
|
254
274
|
return withUpdatedMessage(state, existingId, (message) => {
|
|
255
275
|
if (!isProviderRetryMessage(message)) {
|
|
256
276
|
return message;
|
|
257
277
|
}
|
|
278
|
+
const detail2 = {
|
|
279
|
+
...message.providerRetry,
|
|
280
|
+
attempts: [...message.providerRetry.attempts, attempt],
|
|
281
|
+
// A retry arriving for a settled row means the request recovered once
|
|
282
|
+
// and dropped again — the new attempt reopens the row.
|
|
283
|
+
outcome: "retrying",
|
|
284
|
+
settledAt: void 0
|
|
285
|
+
};
|
|
258
286
|
return {
|
|
259
287
|
...message,
|
|
260
|
-
text,
|
|
261
|
-
providerRetry:
|
|
262
|
-
...message.providerRetry,
|
|
263
|
-
attempts: [...message.providerRetry.attempts, attempt]
|
|
264
|
-
}
|
|
288
|
+
text: formatProviderRetrySummary(detail2),
|
|
289
|
+
providerRetry: detail2
|
|
265
290
|
};
|
|
266
291
|
});
|
|
267
292
|
}
|
|
293
|
+
const detail = {
|
|
294
|
+
assistantMessageId: event.assistantMessageId,
|
|
295
|
+
attempts: [attempt],
|
|
296
|
+
outcome: "retrying"
|
|
297
|
+
};
|
|
268
298
|
const id = createMessageId(state);
|
|
269
299
|
const storedToolCallPath = cloneToolCallPath(toolCallPath);
|
|
270
300
|
const nextState = addMessage(state, {
|
|
271
301
|
id,
|
|
272
302
|
role: "warning",
|
|
273
|
-
text,
|
|
303
|
+
text: formatProviderRetrySummary(detail),
|
|
274
304
|
...storedToolCallPath ? { toolCallPath: storedToolCallPath } : {},
|
|
275
|
-
providerRetry:
|
|
276
|
-
assistantMessageId: event.assistantMessageId,
|
|
277
|
-
attempts: [attempt]
|
|
278
|
-
}
|
|
305
|
+
providerRetry: detail
|
|
279
306
|
});
|
|
280
307
|
return {
|
|
281
308
|
...nextState,
|
|
@@ -285,6 +312,41 @@ function upsertProviderRetryMessage(state, event, toolCallPath, now) {
|
|
|
285
312
|
}
|
|
286
313
|
};
|
|
287
314
|
}
|
|
315
|
+
var REASONING_MESSAGE_ID_SUFFIX = ":reasoning";
|
|
316
|
+
function assistantIdFromStreamMessageId(messageId) {
|
|
317
|
+
return messageId.endsWith(REASONING_MESSAGE_ID_SUFFIX) ? messageId.slice(0, -REASONING_MESSAGE_ID_SUFFIX.length) : messageId;
|
|
318
|
+
}
|
|
319
|
+
function markProviderRetryRecovered(state, assistantMessageId, toolCallPath, now) {
|
|
320
|
+
const rowId = state.providerRetryMessageIds[scopedRegistryKey(toolCallPath, assistantMessageId)];
|
|
321
|
+
if (rowId === void 0) return state;
|
|
322
|
+
return withUpdatedMessage(state, rowId, (message) => {
|
|
323
|
+
if (!isProviderRetryMessage(message) || message.providerRetry.outcome !== "retrying") {
|
|
324
|
+
return message;
|
|
325
|
+
}
|
|
326
|
+
const detail = {
|
|
327
|
+
...message.providerRetry,
|
|
328
|
+
outcome: "recovered",
|
|
329
|
+
settledAt: now
|
|
330
|
+
};
|
|
331
|
+
return { ...message, text: formatProviderRetrySummary(detail), providerRetry: detail };
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function settleAllProviderRetries(state, now) {
|
|
335
|
+
let changed = false;
|
|
336
|
+
const messages = state.messages.map((message) => {
|
|
337
|
+
if (isProviderRetryMessage(message) && message.providerRetry.outcome === "retrying") {
|
|
338
|
+
changed = true;
|
|
339
|
+
const detail = {
|
|
340
|
+
...message.providerRetry,
|
|
341
|
+
outcome: "stopped",
|
|
342
|
+
settledAt: now
|
|
343
|
+
};
|
|
344
|
+
return { ...message, text: formatProviderRetrySummary(detail), providerRetry: detail };
|
|
345
|
+
}
|
|
346
|
+
return message;
|
|
347
|
+
});
|
|
348
|
+
return changed ? { ...state, messages } : state;
|
|
349
|
+
}
|
|
288
350
|
function upsertTrackedTextMessage(state, registryKey, messageId, role, text, mode, live, toolCallPath, timing) {
|
|
289
351
|
const trackingKey = scopedRegistryKey(toolCallPath, messageId);
|
|
290
352
|
const existingId = state[registryKey][trackingKey];
|
|
@@ -608,13 +670,15 @@ function createMessageStateFromMessages(messages) {
|
|
|
608
670
|
toolMessageIds
|
|
609
671
|
};
|
|
610
672
|
}
|
|
611
|
-
function appendUserMessage(state, text, attachmentLabels, createdAt) {
|
|
673
|
+
function appendUserMessage(state, text, attachmentLabels, createdAt, steeredAt, submissionId) {
|
|
612
674
|
return addMessage(state, {
|
|
613
675
|
id: createMessageId(state),
|
|
614
676
|
role: "user",
|
|
615
677
|
text,
|
|
616
678
|
...attachmentLabels && attachmentLabels.length > 0 ? { attachmentLabels } : {},
|
|
617
|
-
...createdAt !== void 0 ? { createdAt } : {}
|
|
679
|
+
...createdAt !== void 0 ? { createdAt } : {},
|
|
680
|
+
...steeredAt !== void 0 ? { steeredAt } : {},
|
|
681
|
+
...submissionId === void 0 ? {} : { submissionId }
|
|
618
682
|
});
|
|
619
683
|
}
|
|
620
684
|
function appendErrorMessage(state, text) {
|
|
@@ -668,12 +732,18 @@ function applyAgentEvent(state, event, toolCallPath, now = Date.now()) {
|
|
|
668
732
|
toolCallPath: nestedPath
|
|
669
733
|
});
|
|
670
734
|
}
|
|
671
|
-
case "assistant_chunk":
|
|
735
|
+
case "assistant_chunk": {
|
|
736
|
+
const recovered = markProviderRetryRecovered(
|
|
737
|
+
state,
|
|
738
|
+
assistantIdFromStreamMessageId(event.messageId),
|
|
739
|
+
toolCallPath,
|
|
740
|
+
now
|
|
741
|
+
);
|
|
672
742
|
if (!event.chunk.text) {
|
|
673
|
-
return
|
|
743
|
+
return recovered;
|
|
674
744
|
}
|
|
675
745
|
return upsertTrackedTextMessage(
|
|
676
|
-
settleLiveReasoning(
|
|
746
|
+
settleLiveReasoning(recovered, toolCallPath, now),
|
|
677
747
|
"assistantMessageIds",
|
|
678
748
|
event.messageId,
|
|
679
749
|
"assistant",
|
|
@@ -682,13 +752,20 @@ function applyAgentEvent(state, event, toolCallPath, now = Date.now()) {
|
|
|
682
752
|
true,
|
|
683
753
|
toolCallPath
|
|
684
754
|
);
|
|
755
|
+
}
|
|
685
756
|
case "assistant_message": {
|
|
757
|
+
const recovered = markProviderRetryRecovered(
|
|
758
|
+
state,
|
|
759
|
+
assistantIdFromStreamMessageId(event.message.id),
|
|
760
|
+
toolCallPath,
|
|
761
|
+
now
|
|
762
|
+
);
|
|
686
763
|
const text = getTextFromMessage(event.message).trim();
|
|
687
764
|
if (text.length === 0) {
|
|
688
|
-
return
|
|
765
|
+
return recovered;
|
|
689
766
|
}
|
|
690
767
|
return upsertTrackedTextMessage(
|
|
691
|
-
settleLiveReasoning(
|
|
768
|
+
settleLiveReasoning(recovered, toolCallPath, now),
|
|
692
769
|
"assistantMessageIds",
|
|
693
770
|
event.message.id,
|
|
694
771
|
"assistant",
|
|
@@ -698,12 +775,18 @@ function applyAgentEvent(state, event, toolCallPath, now = Date.now()) {
|
|
|
698
775
|
toolCallPath
|
|
699
776
|
);
|
|
700
777
|
}
|
|
701
|
-
case "reasoning_chunk":
|
|
778
|
+
case "reasoning_chunk": {
|
|
779
|
+
const recovered = markProviderRetryRecovered(
|
|
780
|
+
state,
|
|
781
|
+
assistantIdFromStreamMessageId(event.messageId),
|
|
782
|
+
toolCallPath,
|
|
783
|
+
now
|
|
784
|
+
);
|
|
702
785
|
if (!event.text) {
|
|
703
|
-
return
|
|
786
|
+
return recovered;
|
|
704
787
|
}
|
|
705
788
|
return upsertTrackedTextMessage(
|
|
706
|
-
|
|
789
|
+
recovered,
|
|
707
790
|
"reasoningMessageIds",
|
|
708
791
|
event.messageId,
|
|
709
792
|
"reasoning",
|
|
@@ -713,13 +796,20 @@ function applyAgentEvent(state, event, toolCallPath, now = Date.now()) {
|
|
|
713
796
|
toolCallPath,
|
|
714
797
|
now
|
|
715
798
|
);
|
|
799
|
+
}
|
|
716
800
|
case "reasoning_message": {
|
|
801
|
+
const recovered = markProviderRetryRecovered(
|
|
802
|
+
state,
|
|
803
|
+
assistantIdFromStreamMessageId(event.message.id),
|
|
804
|
+
toolCallPath,
|
|
805
|
+
now
|
|
806
|
+
);
|
|
717
807
|
const text = getReasoningTextFromMessage(event.message).trim();
|
|
718
808
|
if (text.length === 0) {
|
|
719
|
-
return
|
|
809
|
+
return recovered;
|
|
720
810
|
}
|
|
721
811
|
return upsertTrackedTextMessage(
|
|
722
|
-
|
|
812
|
+
recovered,
|
|
723
813
|
"reasoningMessageIds",
|
|
724
814
|
event.message.id,
|
|
725
815
|
"reasoning",
|
|
@@ -731,12 +821,18 @@ function applyAgentEvent(state, event, toolCallPath, now = Date.now()) {
|
|
|
731
821
|
);
|
|
732
822
|
}
|
|
733
823
|
case "final_message": {
|
|
824
|
+
const recovered = markProviderRetryRecovered(
|
|
825
|
+
state,
|
|
826
|
+
assistantIdFromStreamMessageId(event.message.id),
|
|
827
|
+
toolCallPath,
|
|
828
|
+
now
|
|
829
|
+
);
|
|
734
830
|
const text = getTextFromMessage(event.message).trim();
|
|
735
831
|
if (text.length === 0) {
|
|
736
|
-
return
|
|
832
|
+
return recovered;
|
|
737
833
|
}
|
|
738
834
|
return upsertTrackedTextMessage(
|
|
739
|
-
settleLiveReasoning(
|
|
835
|
+
settleLiveReasoning(recovered, toolCallPath, now),
|
|
740
836
|
"assistantMessageIds",
|
|
741
837
|
event.message.id,
|
|
742
838
|
"assistant",
|
|
@@ -746,6 +842,39 @@ function applyAgentEvent(state, event, toolCallPath, now = Date.now()) {
|
|
|
746
842
|
toolCallPath
|
|
747
843
|
);
|
|
748
844
|
}
|
|
845
|
+
// Tool-use stream/message events prove the retried request resumed even
|
|
846
|
+
// when the response carries no assistant text — mark recovery only; tool
|
|
847
|
+
// rows themselves are created by the tool_call case.
|
|
848
|
+
case "tool_use_chunk":
|
|
849
|
+
return markProviderRetryRecovered(
|
|
850
|
+
state,
|
|
851
|
+
assistantIdFromStreamMessageId(event.messageId),
|
|
852
|
+
toolCallPath,
|
|
853
|
+
now
|
|
854
|
+
);
|
|
855
|
+
case "tool_use_message":
|
|
856
|
+
return markProviderRetryRecovered(
|
|
857
|
+
state,
|
|
858
|
+
assistantIdFromStreamMessageId(event.message.id),
|
|
859
|
+
toolCallPath,
|
|
860
|
+
now
|
|
861
|
+
);
|
|
862
|
+
case "delegated_model_resolved": {
|
|
863
|
+
const id = state.toolMessageIds[scopedToolKey(toolCallPath, event.parentToolCallId)];
|
|
864
|
+
if (id === void 0) return state;
|
|
865
|
+
const model = {
|
|
866
|
+
runId: event.runId,
|
|
867
|
+
turnNumber: event.turnNumber,
|
|
868
|
+
...event.turnId === void 0 ? {} : { turnId: event.turnId },
|
|
869
|
+
modelIdentity: event.modelIdentity,
|
|
870
|
+
source: "resolution"
|
|
871
|
+
};
|
|
872
|
+
return withUpdatedMessage(
|
|
873
|
+
state,
|
|
874
|
+
id,
|
|
875
|
+
(message) => message.role === "tool" ? withDelegatedModel(message, model) : message
|
|
876
|
+
);
|
|
877
|
+
}
|
|
749
878
|
case "tool_call":
|
|
750
879
|
return ensureToolMessage(state, event.toolCall, toolCallPath);
|
|
751
880
|
case "tool_started": {
|
|
@@ -893,6 +1022,7 @@ ${sanitizeMultilineDisplayText(split.body)}`;
|
|
|
893
1022
|
}
|
|
894
1023
|
|
|
895
1024
|
// src/state/session.ts
|
|
1025
|
+
import { isAgentOnlyCoordinatorNotice } from "@nuvin/agent-core/shared";
|
|
896
1026
|
function cloneApproval(approval) {
|
|
897
1027
|
const { toolCallPath, ...rootApproval } = approval;
|
|
898
1028
|
return toolCallPath && toolCallPath.length > 0 ? { ...rootApproval, toolCallPath: [...toolCallPath] } : rootApproval;
|
|
@@ -902,7 +1032,8 @@ function cloneQuestion2(question) {
|
|
|
902
1032
|
return toolCallPath && toolCallPath.length > 0 ? { ...rootQuestion, toolCallPath: [...toolCallPath] } : rootQuestion;
|
|
903
1033
|
}
|
|
904
1034
|
function cloneMessage(message) {
|
|
905
|
-
const {
|
|
1035
|
+
const cloned = message.role === "tool" && message.delegatedModel !== void 0 ? { ...message, delegatedModel: { ...message.delegatedModel } } : message;
|
|
1036
|
+
const { toolCallPath, ...rootMessage } = cloned;
|
|
906
1037
|
return toolCallPath && toolCallPath.length > 0 ? { ...rootMessage, toolCallPath: [...toolCallPath] } : rootMessage;
|
|
907
1038
|
}
|
|
908
1039
|
function cloneBackgroundWorkSummary(summary) {
|
|
@@ -917,6 +1048,8 @@ function cloneBackgroundWorkActive(active) {
|
|
|
917
1048
|
function normalizeResetState(state) {
|
|
918
1049
|
return {
|
|
919
1050
|
...state,
|
|
1051
|
+
recall: copyRecallState(state.recall),
|
|
1052
|
+
activeHumanSubmission: state.activeHumanSubmission === null ? null : structuredClone(state.activeHumanSubmission),
|
|
920
1053
|
backgroundAgents: {
|
|
921
1054
|
...state.backgroundAgents,
|
|
922
1055
|
runningRunIds: [...state.backgroundAgents.runningRunIds]
|
|
@@ -944,6 +1077,8 @@ function normalizeResetState(state) {
|
|
|
944
1077
|
}
|
|
945
1078
|
function createSessionViewState() {
|
|
946
1079
|
return {
|
|
1080
|
+
recall: { identity: null, entries: [] },
|
|
1081
|
+
activeHumanSubmission: null,
|
|
947
1082
|
approval: createApprovalQueueState(),
|
|
948
1083
|
authFlow: null,
|
|
949
1084
|
backgroundAgents: { running: 0, queued: 0, runningRunIds: [] },
|
|
@@ -960,6 +1095,32 @@ function createSessionViewState() {
|
|
|
960
1095
|
}
|
|
961
1096
|
function reduceSessionEvent(state, event) {
|
|
962
1097
|
switch (event.type) {
|
|
1098
|
+
case "active-human-submission":
|
|
1099
|
+
return {
|
|
1100
|
+
...state,
|
|
1101
|
+
activeHumanSubmission: event.key === null ? null : structuredClone(event.key)
|
|
1102
|
+
};
|
|
1103
|
+
case "submission-retracted": {
|
|
1104
|
+
const { key, rawText } = event.result;
|
|
1105
|
+
const identity = state.recall.identity;
|
|
1106
|
+
if (identity === null || identity.profile !== key.identity.profile || identity.workspace !== key.identity.workspace || identity.sessionId !== key.identity.sessionId)
|
|
1107
|
+
return state;
|
|
1108
|
+
const recall = appendRecallEntry(state.recall, { key, rawText, retracted: true });
|
|
1109
|
+
const removed = new Set(event.removedMessageIds);
|
|
1110
|
+
return {
|
|
1111
|
+
...state,
|
|
1112
|
+
recall,
|
|
1113
|
+
activeHumanSubmission: state.activeHumanSubmission?.submissionId === key.submissionId ? null : state.activeHumanSubmission,
|
|
1114
|
+
messages: {
|
|
1115
|
+
...state.messages,
|
|
1116
|
+
messages: state.messages.messages.filter(
|
|
1117
|
+
(message) => !(message.role === "user" && message.submissionId === key.submissionId && removed.has(message.id))
|
|
1118
|
+
)
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
case "recall-state":
|
|
1123
|
+
return { ...state, recall: copyRecallState(event.recall) };
|
|
963
1124
|
case "agent-event": {
|
|
964
1125
|
let messages = applyAgentEvent(
|
|
965
1126
|
state.messages,
|
|
@@ -988,7 +1149,14 @@ function reduceSessionEvent(state, event) {
|
|
|
988
1149
|
case "user-message":
|
|
989
1150
|
return {
|
|
990
1151
|
...state,
|
|
991
|
-
messages: appendUserMessage(
|
|
1152
|
+
messages: appendUserMessage(
|
|
1153
|
+
state.messages,
|
|
1154
|
+
event.text,
|
|
1155
|
+
event.attachmentLabels,
|
|
1156
|
+
event.at,
|
|
1157
|
+
event.steeredAt,
|
|
1158
|
+
event.submissionId
|
|
1159
|
+
)
|
|
992
1160
|
};
|
|
993
1161
|
case "approval-requested":
|
|
994
1162
|
return {
|
|
@@ -1031,7 +1199,7 @@ function reduceSessionEvent(state, event) {
|
|
|
1031
1199
|
question: removeQuestion(state.question, event.questionId, event.toolCallPath)
|
|
1032
1200
|
};
|
|
1033
1201
|
case "turn-status": {
|
|
1034
|
-
const messages = event.busy ? state.messages : settleAllLiveReasoning(state.messages, event.at);
|
|
1202
|
+
const messages = event.busy ? state.messages : settleAllProviderRetries(settleAllLiveReasoning(state.messages, event.at), event.at);
|
|
1035
1203
|
return {
|
|
1036
1204
|
...state,
|
|
1037
1205
|
busy: event.busy,
|
|
@@ -1052,6 +1220,7 @@ function reduceSessionEvent(state, event) {
|
|
|
1052
1220
|
case "background-work-active":
|
|
1053
1221
|
return { ...state, backgroundWork: cloneBackgroundWorkActive(event.active) };
|
|
1054
1222
|
case "info": {
|
|
1223
|
+
if (isAgentOnlyCoordinatorNotice(event)) return state;
|
|
1055
1224
|
const report = resolveSubagentReport(event.message, event.subagentReport);
|
|
1056
1225
|
return {
|
|
1057
1226
|
...state,
|
|
@@ -1062,7 +1231,7 @@ function reduceSessionEvent(state, event) {
|
|
|
1062
1231
|
return {
|
|
1063
1232
|
...state,
|
|
1064
1233
|
messages: appendErrorMessage(
|
|
1065
|
-
settleAllLiveReasoning(state.messages, event.at),
|
|
1234
|
+
settleAllProviderRetries(settleAllLiveReasoning(state.messages, event.at), event.at),
|
|
1066
1235
|
event.message
|
|
1067
1236
|
)
|
|
1068
1237
|
};
|
|
@@ -1080,6 +1249,36 @@ function reduceSessionEvent(state, event) {
|
|
|
1080
1249
|
return state;
|
|
1081
1250
|
}
|
|
1082
1251
|
}
|
|
1252
|
+
function appendRecallEntry(state, entry) {
|
|
1253
|
+
const identity = state.identity;
|
|
1254
|
+
if (identity === null || identity.profile !== entry.key.identity.profile || identity.workspace !== entry.key.identity.workspace || identity.sessionId !== entry.key.identity.sessionId)
|
|
1255
|
+
return state;
|
|
1256
|
+
if (typeof entry.key.submissionId !== "string" || entry.key.submissionId.trim().length === 0 || typeof entry.rawText !== "string" || typeof entry.retracted !== "boolean") {
|
|
1257
|
+
throw new Error("Invalid recall entry");
|
|
1258
|
+
}
|
|
1259
|
+
const index = state.entries.findIndex((item) => item.key.submissionId === entry.key.submissionId);
|
|
1260
|
+
const previous = state.entries[index];
|
|
1261
|
+
if (previous !== void 0) {
|
|
1262
|
+
if (previous.rawText !== entry.rawText) throw new Error("Conflicting submission raw text");
|
|
1263
|
+
if (previous.retracted || !entry.retracted) return state;
|
|
1264
|
+
const entries = [...state.entries];
|
|
1265
|
+
entries[index] = { ...previous, retracted: true };
|
|
1266
|
+
return { ...state, entries };
|
|
1267
|
+
}
|
|
1268
|
+
return { ...state, entries: [structuredClone(entry), ...state.entries] };
|
|
1269
|
+
}
|
|
1270
|
+
function copyRecallState(recall) {
|
|
1271
|
+
let copy = {
|
|
1272
|
+
identity: recall.identity === null ? null : { ...recall.identity },
|
|
1273
|
+
entries: []
|
|
1274
|
+
};
|
|
1275
|
+
for (const entry of [...recall.entries].reverse()) {
|
|
1276
|
+
if (copy.identity === null || copy.identity.profile !== entry.key.identity.profile || copy.identity.workspace !== entry.key.identity.workspace || copy.identity.sessionId !== entry.key.identity.sessionId)
|
|
1277
|
+
throw new Error("Recall entry identity mismatch");
|
|
1278
|
+
copy = appendRecallEntry(copy, entry);
|
|
1279
|
+
}
|
|
1280
|
+
return copy;
|
|
1281
|
+
}
|
|
1083
1282
|
|
|
1084
1283
|
export {
|
|
1085
1284
|
BACKGROUNDLESS_TOOLS,
|
|
@@ -1094,6 +1293,8 @@ export {
|
|
|
1094
1293
|
asJsonObject,
|
|
1095
1294
|
scopedToolKey,
|
|
1096
1295
|
isProviderRetryMessage,
|
|
1296
|
+
withDelegatedModel,
|
|
1297
|
+
settleAllProviderRetries,
|
|
1097
1298
|
settleAllLiveReasoning,
|
|
1098
1299
|
summarizeToolCallInput,
|
|
1099
1300
|
setToolMessageStatus,
|
|
@@ -1115,5 +1316,7 @@ export {
|
|
|
1115
1316
|
splitSanitizableDelivery,
|
|
1116
1317
|
sanitizeCoordinatorDeliveryText,
|
|
1117
1318
|
createSessionViewState,
|
|
1118
|
-
reduceSessionEvent
|
|
1319
|
+
reduceSessionEvent,
|
|
1320
|
+
appendRecallEntry,
|
|
1321
|
+
copyRecallState
|
|
1119
1322
|
};
|