@mirasoth/soothe-client 0.1.0 → 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/README.md +59 -43
- package/dist/chunk-U6RMINYV.js +1883 -0
- package/dist/chunk-U6RMINYV.js.map +1 -0
- package/dist/client-UNPC32NQ.js +7 -0
- package/dist/index.cjs +3677 -584
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1279 -280
- package/dist/index.d.ts +1279 -280
- package/dist/index.js +2098 -243
- package/dist/index.js.map +1 -1
- package/package.json +37 -8
- package/dist/chunk-OMAC7LA7.js +0 -647
- package/dist/chunk-OMAC7LA7.js.map +0 -1
- package/dist/client-QS23U6WX.js +0 -7
- /package/dist/{client-QS23U6WX.js.map → client-UNPC32NQ.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,195 +1,243 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CLIENT_VERSION,
|
|
2
3
|
Client,
|
|
4
|
+
ConnectionError,
|
|
5
|
+
DEFAULT_CLIENT_CAPABILITIES,
|
|
6
|
+
DEFAULT_DELIVERABLE_PHASES,
|
|
7
|
+
DaemonError,
|
|
8
|
+
DisconnectCause,
|
|
9
|
+
EventAutopilotGoalCompleted,
|
|
10
|
+
EventAutopilotGoalCreated,
|
|
11
|
+
EventAutopilotGoalProgress,
|
|
12
|
+
EventAutopilotGoalStatus,
|
|
13
|
+
EventAutopilotWorkerAssigned,
|
|
14
|
+
EventAutopilotWorkerUnassigned,
|
|
15
|
+
EventCardCreated,
|
|
16
|
+
EventCardReplayBegin,
|
|
17
|
+
EventCardReplayEnd,
|
|
18
|
+
EventExploreCompleted,
|
|
19
|
+
EventExploreMilestone,
|
|
20
|
+
EventExploreStarted,
|
|
21
|
+
EventExploreStepCompleted,
|
|
22
|
+
EventFinalReport,
|
|
23
|
+
EventGeneralFailed,
|
|
24
|
+
EventLoopReattachedWire,
|
|
25
|
+
EventMessageReceived,
|
|
26
|
+
EventMessageSent,
|
|
27
|
+
EventPlanCreated,
|
|
28
|
+
EventReplayComplete,
|
|
29
|
+
EventStrangeLoopCompleted,
|
|
30
|
+
EventStrangeLoopContextCompacted,
|
|
31
|
+
EventStrangeLoopPlanDecision,
|
|
32
|
+
EventStrangeLoopReasoned,
|
|
33
|
+
EventStrangeLoopStarted,
|
|
34
|
+
EventStrangeLoopStepCompleted,
|
|
35
|
+
EventStrangeLoopStepQueued,
|
|
36
|
+
EventStrangeLoopStepStarted,
|
|
37
|
+
EventStreamToolCallUpdate,
|
|
38
|
+
EventTacitusCompleted,
|
|
39
|
+
EventTacitusGatherSummary,
|
|
40
|
+
EventTacitusStarted,
|
|
41
|
+
EventToolCallUpdatesBatch,
|
|
42
|
+
EventToolCompleted,
|
|
43
|
+
EventToolError,
|
|
44
|
+
EventToolStarted,
|
|
45
|
+
INTENT_HINT_EMBED,
|
|
46
|
+
INTENT_HINT_IMAGE_TO_TEXT,
|
|
47
|
+
INTENT_HINT_OCR,
|
|
48
|
+
INTENT_HINT_TEXT_COMPLETION,
|
|
49
|
+
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
50
|
+
PROTO_VERSION,
|
|
51
|
+
REMOVED_INTENT_HINTS,
|
|
52
|
+
ReconnectError,
|
|
53
|
+
STREAM_END,
|
|
54
|
+
StaleLoopError,
|
|
55
|
+
TimeoutError,
|
|
56
|
+
VerbosityTier,
|
|
57
|
+
classifyEventVerbosity,
|
|
58
|
+
connectionInitEnvelope,
|
|
3
59
|
decodeMessage,
|
|
4
60
|
defaultConfig,
|
|
61
|
+
disconnectCauseName,
|
|
62
|
+
disconnectEnvelope,
|
|
5
63
|
encodeMessage,
|
|
6
64
|
extractSootheLoopID,
|
|
65
|
+
inboundNeedsDeliveryAck,
|
|
66
|
+
isCompletionEvent,
|
|
67
|
+
isSubagentProgressEvent,
|
|
68
|
+
isTurnEndCustomData,
|
|
69
|
+
isTurnProgressChunk,
|
|
70
|
+
isValidVerbosityLevel,
|
|
7
71
|
loadConfigFromEnv,
|
|
8
72
|
newLoopInputMessage,
|
|
9
73
|
newLoopNewMessage,
|
|
10
74
|
newLoopSubscribeMessage,
|
|
11
75
|
newRequestID,
|
|
12
|
-
|
|
13
|
-
|
|
76
|
+
notificationEnvelope,
|
|
77
|
+
parseNamespace,
|
|
78
|
+
pingEnvelope,
|
|
79
|
+
pongEnvelope,
|
|
80
|
+
requestEnvelope,
|
|
81
|
+
shouldShow,
|
|
82
|
+
splitWirePayload,
|
|
83
|
+
subscribeEnvelope,
|
|
84
|
+
unsubscribeEnvelope,
|
|
85
|
+
validateLoopInputIntentHint
|
|
86
|
+
} from "./chunk-U6RMINYV.js";
|
|
14
87
|
|
|
15
|
-
// src/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
88
|
+
// src/session.ts
|
|
89
|
+
async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
|
|
90
|
+
const cfg = config ?? defaultConfig();
|
|
91
|
+
let loopId = (resumeLoopId ?? "").trim();
|
|
92
|
+
if (!loopId) {
|
|
93
|
+
const env = newLoopNewMessage(loopNew);
|
|
94
|
+
const newResp = await client.requestResponse(
|
|
95
|
+
env.method,
|
|
96
|
+
env.params ?? {},
|
|
97
|
+
"loop_new",
|
|
98
|
+
cfg.loopStatusTimeout
|
|
99
|
+
);
|
|
100
|
+
loopId = String(newResp.loop_id ?? "").trim();
|
|
101
|
+
if (!loopId) {
|
|
102
|
+
throw new Error("loop_new response missing loop_id");
|
|
103
|
+
}
|
|
26
104
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
105
|
+
await client.subscribe(
|
|
106
|
+
"loop_events",
|
|
107
|
+
{ loop_id: loopId, verbosity: cfg.verbosityLevel },
|
|
108
|
+
cfg.subscriptionTimeout
|
|
109
|
+
);
|
|
110
|
+
return loopId;
|
|
111
|
+
}
|
|
112
|
+
async function waitDaemonReady(client, timeout) {
|
|
113
|
+
if (client.isConnected()) return;
|
|
114
|
+
const deadline = Date.now() + timeout;
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
const remaining = deadline - Date.now();
|
|
117
|
+
if (remaining <= 0) break;
|
|
118
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
119
|
+
if (ev === null) break;
|
|
120
|
+
if (ev.type === "connection_ack") {
|
|
121
|
+
const result = ev.result ?? {};
|
|
122
|
+
const state = result.readiness_state;
|
|
123
|
+
if (state === "ready") return;
|
|
124
|
+
throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
|
|
125
|
+
}
|
|
37
126
|
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
127
|
+
throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
|
|
128
|
+
}
|
|
129
|
+
async function waitLoopStatusWithID(client, timeout) {
|
|
130
|
+
const deadline = Date.now() + timeout;
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
const remaining = deadline - Date.now();
|
|
133
|
+
if (remaining <= 0) break;
|
|
134
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
135
|
+
if (ev === null) break;
|
|
136
|
+
if (ev.type === "error") {
|
|
137
|
+
const errObj = ev.error ?? {};
|
|
138
|
+
throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
|
|
139
|
+
}
|
|
140
|
+
if (ev.type === "status") {
|
|
141
|
+
const lid = ev.loop_id;
|
|
142
|
+
if (lid && lid !== "") {
|
|
143
|
+
return ev;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
47
146
|
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
147
|
+
throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
|
|
148
|
+
}
|
|
149
|
+
async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
|
|
150
|
+
const deadline = Date.now() + timeout;
|
|
151
|
+
while (Date.now() < deadline) {
|
|
152
|
+
const remaining = deadline - Date.now();
|
|
153
|
+
if (remaining <= 0) break;
|
|
154
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
155
|
+
if (ev === null) break;
|
|
156
|
+
if (ev.type === "next") {
|
|
157
|
+
const payload = ev.payload ?? {};
|
|
158
|
+
const lid = String(payload.loop_id ?? "");
|
|
159
|
+
if (lid === wantLoopID && payload.success === true) return;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ev.type === "error") {
|
|
163
|
+
const errObj = ev.error ?? {};
|
|
164
|
+
throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
|
|
165
|
+
}
|
|
67
166
|
}
|
|
68
|
-
|
|
69
|
-
return tier <= level;
|
|
167
|
+
throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
|
|
70
168
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
169
|
+
async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
170
|
+
const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
|
|
171
|
+
const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
|
|
172
|
+
let lastErr = null;
|
|
173
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
174
|
+
try {
|
|
175
|
+
await client.connect();
|
|
176
|
+
return;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
lastErr = err;
|
|
179
|
+
}
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
181
|
+
}
|
|
182
|
+
throw new Error(
|
|
183
|
+
`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
|
|
184
|
+
);
|
|
73
185
|
}
|
|
74
186
|
|
|
75
|
-
// src/
|
|
76
|
-
var
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
var EventReplayComplete = "replay_complete";
|
|
85
|
-
var EventLoopReattachedWire = "loop_reattached";
|
|
86
|
-
var EventToolStarted = "soothe.tool.execution.started";
|
|
87
|
-
var EventToolCompleted = "soothe.tool.execution.completed";
|
|
88
|
-
var EventToolError = "soothe.tool.execution.error";
|
|
89
|
-
var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
|
|
90
|
-
var EventToolCallUpdatesBatch = "tool_call_updates_batch";
|
|
91
|
-
var EventAgentLoopStarted = "soothe.cognition.agent_loop.started";
|
|
92
|
-
var EventAgentLoopIterated = "soothe.cognition.agent_loop.iterated";
|
|
93
|
-
var EventAgentLoopCompleted = "soothe.cognition.agent_loop.completed";
|
|
94
|
-
var EventAgentLoopReasoned = "soothe.cognition.agent_loop.reasoned";
|
|
95
|
-
var EventMessageReceived = "soothe.protocol.message.received";
|
|
96
|
-
var EventMessageSent = "soothe.protocol.message.sent";
|
|
97
|
-
var EventFinalReport = "soothe.output.autonomous.final_report.reported";
|
|
98
|
-
var EventGeneralFailed = "soothe.error.general.failed";
|
|
99
|
-
function parseNamespace(ns) {
|
|
100
|
-
const parts = splitNamespace(ns);
|
|
101
|
-
if (parts.length < 4 || parts[0] !== "soothe") {
|
|
102
|
-
return null;
|
|
187
|
+
// src/command_client.ts
|
|
188
|
+
var CommandClient = class {
|
|
189
|
+
url;
|
|
190
|
+
timeoutMs;
|
|
191
|
+
config;
|
|
192
|
+
constructor(url, opts) {
|
|
193
|
+
this.url = url;
|
|
194
|
+
this.timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e4;
|
|
195
|
+
this.config = opts?.config ?? defaultConfig();
|
|
103
196
|
}
|
|
104
|
-
|
|
105
|
-
|
|
197
|
+
async withClient(fn) {
|
|
198
|
+
const client = new Client(this.url, this.config);
|
|
199
|
+
try {
|
|
200
|
+
await connectWithRetries(client, 5, 250);
|
|
201
|
+
return await fn(client);
|
|
202
|
+
} finally {
|
|
203
|
+
client.close();
|
|
204
|
+
}
|
|
106
205
|
}
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < ns.length; i++) {
|
|
113
|
-
if (ns[i] === ".") {
|
|
114
|
-
parts.push(ns.slice(start, i));
|
|
115
|
-
start = i + 1;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
parts.push(ns.slice(start));
|
|
119
|
-
return parts;
|
|
120
|
-
}
|
|
121
|
-
function classifyEventVerbosity(eventTypeOrNamespace) {
|
|
122
|
-
const parsed = parseNamespace(eventTypeOrNamespace);
|
|
123
|
-
if (!parsed) {
|
|
124
|
-
return classifyByEventTypeString(eventTypeOrNamespace);
|
|
125
|
-
}
|
|
126
|
-
return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
|
|
127
|
-
}
|
|
128
|
-
function classifyByDomainAndComponent(domain, _component, full) {
|
|
129
|
-
switch (domain) {
|
|
130
|
-
case "cognition":
|
|
131
|
-
return 1 /* Normal */;
|
|
132
|
-
case "protocol":
|
|
133
|
-
return 2 /* Detailed */;
|
|
134
|
-
case "tool":
|
|
135
|
-
return 99 /* Internal */;
|
|
136
|
-
case "subagent":
|
|
137
|
-
return classifySubagentEvent(full);
|
|
138
|
-
case "output":
|
|
139
|
-
case "error":
|
|
140
|
-
return 0 /* Quiet */;
|
|
141
|
-
default:
|
|
142
|
-
return 1 /* Normal */;
|
|
206
|
+
/** Generic one-shot RPC. */
|
|
207
|
+
async request(method, params = {}) {
|
|
208
|
+
return this.withClient(
|
|
209
|
+
(client) => client.requestResponse(method, params, void 0, this.timeoutMs)
|
|
210
|
+
);
|
|
143
211
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
switch (parsed.action) {
|
|
149
|
-
case "started":
|
|
150
|
-
case "completed":
|
|
151
|
-
return 1 /* Normal */;
|
|
152
|
-
default:
|
|
153
|
-
return 2 /* Detailed */;
|
|
212
|
+
async jobCreate(goal, workspace = "") {
|
|
213
|
+
const params = { goal };
|
|
214
|
+
if (workspace) params.workspace = workspace;
|
|
215
|
+
return this.request("job_create", params);
|
|
154
216
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
|
|
158
|
-
return 0 /* Quiet */;
|
|
217
|
+
async jobStatus(jobId) {
|
|
218
|
+
return this.request("job_status", { job_id: jobId });
|
|
159
219
|
}
|
|
160
|
-
|
|
161
|
-
return
|
|
220
|
+
async jobCancel(jobId) {
|
|
221
|
+
return this.request("job_cancel", { job_id: jobId });
|
|
162
222
|
}
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
function isSubagentProgressEvent(eventType) {
|
|
169
|
-
const parsed = parseNamespace(eventType);
|
|
170
|
-
if (!parsed || parsed.domain !== "subagent") {
|
|
171
|
-
return false;
|
|
223
|
+
async cronAdd(text, priority = 0) {
|
|
224
|
+
const params = { text };
|
|
225
|
+
if (priority > 0) params.priority = priority;
|
|
226
|
+
return this.request("cron_add", params);
|
|
172
227
|
}
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
EventPlanCreated,
|
|
180
|
-
EventExploreStarted,
|
|
181
|
-
EventExploreCompleted,
|
|
182
|
-
EventTacitusStarted,
|
|
183
|
-
EventTacitusCompleted,
|
|
184
|
-
EventGeneralFailed
|
|
185
|
-
]);
|
|
228
|
+
async cronList(status = "") {
|
|
229
|
+
const params = {};
|
|
230
|
+
if (status) params.status = status;
|
|
231
|
+
return this.request("cron_list", params);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
186
234
|
|
|
187
235
|
// src/helpers.ts
|
|
188
236
|
async function checkDaemonStatus(client, timeout) {
|
|
189
|
-
return client.requestResponse(
|
|
237
|
+
return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
|
|
190
238
|
}
|
|
191
239
|
async function isDaemonLive(wsURL, timeout) {
|
|
192
|
-
const { Client: Client2 } = await import("./client-
|
|
240
|
+
const { Client: Client2 } = await import("./client-UNPC32NQ.js");
|
|
193
241
|
const t = timeout ?? 5e3;
|
|
194
242
|
const client = new Client2(wsURL, defaultConfig());
|
|
195
243
|
try {
|
|
@@ -207,131 +255,1885 @@ async function isDaemonLive(wsURL, timeout) {
|
|
|
207
255
|
}
|
|
208
256
|
}
|
|
209
257
|
async function requestDaemonShutdown(client, timeout) {
|
|
210
|
-
const resp = await client.requestResponse(
|
|
258
|
+
const resp = await client.requestResponse(
|
|
259
|
+
"daemon_shutdown",
|
|
260
|
+
{},
|
|
261
|
+
"daemon_shutdown",
|
|
262
|
+
timeout ?? 1e4
|
|
263
|
+
);
|
|
211
264
|
if (resp.status !== "acknowledged") {
|
|
212
265
|
throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
|
|
213
266
|
}
|
|
214
267
|
}
|
|
215
268
|
async function fetchSkillsCatalog(client, timeout) {
|
|
216
|
-
const resp = await client.requestResponse(
|
|
269
|
+
const resp = await client.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
|
|
217
270
|
const skillsRaw = resp.skills;
|
|
218
271
|
if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
|
|
219
272
|
return skillsRaw.filter((s) => typeof s === "object" && s !== null);
|
|
220
273
|
}
|
|
221
274
|
async function fetchConfigSection(client, section, timeout) {
|
|
222
|
-
const resp = await client.requestResponse(
|
|
275
|
+
const resp = await client.requestResponse(
|
|
276
|
+
"config_get",
|
|
277
|
+
{ section },
|
|
278
|
+
"config_get",
|
|
279
|
+
timeout ?? 5e3
|
|
280
|
+
);
|
|
223
281
|
const sec = resp[section];
|
|
224
282
|
if (sec && typeof sec === "object") {
|
|
225
283
|
return sec;
|
|
226
284
|
}
|
|
227
285
|
return resp;
|
|
228
286
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
287
|
+
async function requestDaemonConfigReload(client, timeout) {
|
|
288
|
+
return client.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
|
|
289
|
+
}
|
|
290
|
+
async function fetchLoopHistory(client, loopID, timeout) {
|
|
291
|
+
return client.requestResponse(
|
|
292
|
+
"loop_history_fetch",
|
|
293
|
+
{ loop_id: loopID },
|
|
294
|
+
"loop_history_fetch",
|
|
295
|
+
timeout ?? 15e3
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
async function authenticate(client, accessKey, secretKey, timeout) {
|
|
299
|
+
return client.requestResponse(
|
|
300
|
+
"auth",
|
|
301
|
+
{ access_key: accessKey, secret_key: secretKey },
|
|
302
|
+
"auth",
|
|
303
|
+
timeout ?? 15e3
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
async function refreshAuthToken(client, refreshToken, timeout) {
|
|
307
|
+
return client.requestResponse(
|
|
308
|
+
"auth_refresh",
|
|
309
|
+
{ refresh_token: refreshToken },
|
|
310
|
+
"auth_refresh",
|
|
311
|
+
timeout ?? 15e3
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
async function fetchLoopCards(client, loopID, timeout) {
|
|
315
|
+
return client.fetchLoopCards(loopID, timeout);
|
|
316
|
+
}
|
|
317
|
+
async function fetchLoopMessages(client, loopID, opts) {
|
|
318
|
+
return client.getLoopMessages(
|
|
319
|
+
loopID,
|
|
320
|
+
opts?.limit,
|
|
321
|
+
opts?.offset,
|
|
322
|
+
opts?.includeEvents,
|
|
323
|
+
opts?.timeout
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
async function connectedWebsocket(wsUrl, fn, timeoutMs = 3e4) {
|
|
327
|
+
const { Client: Client2 } = await import("./client-UNPC32NQ.js");
|
|
328
|
+
const client = new Client2(wsUrl, defaultConfig());
|
|
329
|
+
const deadline = Date.now() + timeoutMs;
|
|
330
|
+
try {
|
|
331
|
+
await client.connect();
|
|
332
|
+
while (!client.isConnected() && Date.now() < deadline) {
|
|
333
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
334
|
+
}
|
|
335
|
+
if (!client.isConnected()) {
|
|
336
|
+
throw new Error("Timed out waiting for daemon handshake");
|
|
337
|
+
}
|
|
338
|
+
return await fn(client);
|
|
339
|
+
} finally {
|
|
340
|
+
client.close();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function protocol1Rpc(wsUrl, method, params = null, opts = {}) {
|
|
344
|
+
const mode = opts.mode ?? "request";
|
|
345
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
346
|
+
try {
|
|
347
|
+
return await connectedWebsocket(
|
|
348
|
+
wsUrl,
|
|
349
|
+
async (client) => {
|
|
350
|
+
if (mode === "notify") {
|
|
351
|
+
await client.notify(method, params ?? {});
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
if (mode === "subscribe") {
|
|
355
|
+
const subId = await client.subscribe(
|
|
356
|
+
method,
|
|
357
|
+
params ?? {},
|
|
358
|
+
timeoutMs
|
|
359
|
+
);
|
|
360
|
+
return { subscription_id: subId };
|
|
361
|
+
}
|
|
362
|
+
const result = await client.requestResponse(
|
|
363
|
+
method,
|
|
364
|
+
params ?? {},
|
|
365
|
+
method,
|
|
366
|
+
timeoutMs
|
|
367
|
+
);
|
|
368
|
+
return result && typeof result === "object" ? result : { result };
|
|
369
|
+
},
|
|
370
|
+
timeoutMs
|
|
241
371
|
);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
372
|
+
} catch (exc) {
|
|
373
|
+
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
374
|
+
if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
|
|
375
|
+
return { error: "Timed out waiting for daemon response" };
|
|
245
376
|
}
|
|
377
|
+
if (msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("dial")) {
|
|
378
|
+
return { error: `Connection error: ${msg}` };
|
|
379
|
+
}
|
|
380
|
+
return { error: msg };
|
|
246
381
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/appkit/broadcaster.ts
|
|
385
|
+
var SUBSCRIBER_QUEUE_CAP = 100;
|
|
386
|
+
var SSEBroadcaster = class {
|
|
387
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
388
|
+
nextSubID = 0;
|
|
389
|
+
/** Creates an empty broadcaster. */
|
|
390
|
+
constructor() {
|
|
254
391
|
}
|
|
255
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Registers a new subscriber channel for a session id. Returns an async
|
|
394
|
+
* iterable the subscriber reads events from. Unsubscribe via
|
|
395
|
+
* `unsubscribe()` or `close()`.
|
|
396
|
+
*/
|
|
397
|
+
subscribe(sessionID) {
|
|
398
|
+
const subID = String(this.nextSubID++);
|
|
399
|
+
const sub = { queue: [], waiters: [], closed: false };
|
|
400
|
+
let subs = this.subscribers.get(sessionID);
|
|
401
|
+
if (!subs) {
|
|
402
|
+
subs = /* @__PURE__ */ new Map();
|
|
403
|
+
this.subscribers.set(sessionID, subs);
|
|
404
|
+
}
|
|
405
|
+
subs.set(subID, sub);
|
|
406
|
+
const iterable = {
|
|
407
|
+
[Symbol.asyncIterator]() {
|
|
408
|
+
return {
|
|
409
|
+
next() {
|
|
410
|
+
if (sub.queue.length > 0) {
|
|
411
|
+
return Promise.resolve({ value: sub.queue.shift(), done: false });
|
|
412
|
+
}
|
|
413
|
+
if (sub.closed) {
|
|
414
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
415
|
+
}
|
|
416
|
+
return new Promise((resolve) => {
|
|
417
|
+
sub.waiters.push((ev) => {
|
|
418
|
+
if (ev === null) {
|
|
419
|
+
resolve({ value: void 0, done: true });
|
|
420
|
+
} else {
|
|
421
|
+
resolve({ value: ev, done: false });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
return { iterable, id: subID };
|
|
430
|
+
}
|
|
431
|
+
/** Removes a subscriber by id and closes its iterable. Safe if unknown. */
|
|
432
|
+
unsubscribe(sessionID, subID) {
|
|
433
|
+
const subs = this.subscribers.get(sessionID);
|
|
434
|
+
if (!subs) return;
|
|
435
|
+
const sub = subs.get(subID);
|
|
436
|
+
if (!sub) return;
|
|
437
|
+
sub.closed = true;
|
|
438
|
+
for (const w of sub.waiters) w(null);
|
|
439
|
+
sub.waiters = [];
|
|
440
|
+
subs.delete(subID);
|
|
441
|
+
if (subs.size === 0) this.subscribers.delete(sessionID);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Sends an event to all subscribers for a session id. Non-blocking: a full
|
|
445
|
+
* subscriber queue is skipped (drop-on-full) so one slow consumer cannot
|
|
446
|
+
* block the others.
|
|
447
|
+
*/
|
|
448
|
+
broadcast(sessionID, event) {
|
|
449
|
+
const subs = this.subscribers.get(sessionID);
|
|
450
|
+
if (!subs) return;
|
|
451
|
+
for (const sub of subs.values()) {
|
|
452
|
+
if (sub.closed) continue;
|
|
453
|
+
if (sub.waiters.length > 0) {
|
|
454
|
+
const w = sub.waiters.shift();
|
|
455
|
+
w(event);
|
|
456
|
+
} else if (sub.queue.length < SUBSCRIBER_QUEUE_CAP) {
|
|
457
|
+
sub.queue.push(event);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/** Closes all subscribers for a session id and removes the entry. */
|
|
462
|
+
close(sessionID) {
|
|
463
|
+
const subs = this.subscribers.get(sessionID);
|
|
464
|
+
if (!subs) return;
|
|
465
|
+
for (const sub of subs.values()) {
|
|
466
|
+
sub.closed = true;
|
|
467
|
+
for (const w of sub.waiters) w(null);
|
|
468
|
+
sub.waiters = [];
|
|
469
|
+
}
|
|
470
|
+
this.subscribers.delete(sessionID);
|
|
471
|
+
}
|
|
472
|
+
/** Closes every subscriber channel across all sessions. */
|
|
473
|
+
closeAll() {
|
|
474
|
+
for (const [sessionID, subs] of this.subscribers) {
|
|
475
|
+
for (const sub of subs.values()) {
|
|
476
|
+
sub.closed = true;
|
|
477
|
+
for (const w of sub.waiters) w(null);
|
|
478
|
+
sub.waiters = [];
|
|
479
|
+
}
|
|
480
|
+
this.subscribers.delete(sessionID);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/appkit/thinking_step.ts
|
|
486
|
+
var MAX_THINKING_STEP_RUNES = 280;
|
|
487
|
+
var DEFAULT_THINKING_STEP_EVENTS = /* @__PURE__ */ new Set([
|
|
488
|
+
"soothe.cognition.plan.step.started",
|
|
489
|
+
"soothe.cognition.plan.step.completed",
|
|
490
|
+
"soothe.cognition.plan.step.failed",
|
|
491
|
+
"soothe.lifecycle.iteration.started",
|
|
492
|
+
"soothe.agent.loop.step.started",
|
|
493
|
+
"soothe.agent.loop.started",
|
|
494
|
+
"soothe.cognition.plan.batch.started",
|
|
495
|
+
"soothe.cognition.plan.created",
|
|
496
|
+
"soothe.cognition.goal.created",
|
|
497
|
+
"soothe.tool.execution.started"
|
|
498
|
+
]);
|
|
499
|
+
function extractThinkingStep(eventType, data, allow) {
|
|
500
|
+
if (!eventType || !data) return ["", false];
|
|
501
|
+
const et = eventType.trim();
|
|
502
|
+
if (!et) return ["", false];
|
|
503
|
+
const allowlist = allow ?? DEFAULT_THINKING_STEP_EVENTS;
|
|
504
|
+
if (!allowlist.has(et)) return ["", false];
|
|
505
|
+
let line = "";
|
|
506
|
+
switch (et) {
|
|
507
|
+
case "soothe.cognition.plan.step.started":
|
|
508
|
+
line = formatPlanStepLine(data, "");
|
|
509
|
+
break;
|
|
510
|
+
case "soothe.cognition.plan.step.completed":
|
|
511
|
+
line = formatPlanStepLine(data, "done");
|
|
512
|
+
break;
|
|
513
|
+
case "soothe.cognition.plan.step.failed": {
|
|
514
|
+
const stepID = strField(data, "step_id");
|
|
515
|
+
const errMsg = strField(data, "error");
|
|
516
|
+
if (stepID && errMsg) line = `Step ${stepID} failed: ${errMsg}`;
|
|
517
|
+
else if (stepID) line = `Step ${stepID} failed`;
|
|
518
|
+
else if (errMsg) line = `Step failed: ${errMsg}`;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case "soothe.agent.loop.step.started":
|
|
522
|
+
line = formatAgentStepLine(data, "");
|
|
523
|
+
break;
|
|
524
|
+
case "soothe.cognition.plan.batch.started": {
|
|
525
|
+
const n = data["parallel_count"];
|
|
526
|
+
if (typeof n === "number" && n > 0) line = `Running ${Math.floor(n)} steps in parallel`;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "soothe.cognition.plan.created":
|
|
530
|
+
case "soothe.agent.loop.started": {
|
|
531
|
+
const g = strField(data, "goal");
|
|
532
|
+
if (g) line = "Goal: " + g;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case "soothe.cognition.goal.created": {
|
|
536
|
+
const g = strField(data, "friendly_message", "description");
|
|
537
|
+
if (g) line = "Goal: " + g;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case "soothe.lifecycle.iteration.started": {
|
|
541
|
+
const g = strField(data, "goal_description");
|
|
542
|
+
if (g) line = "Iteration: " + g;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case "soothe.tool.execution.started": {
|
|
546
|
+
const name = strField(data, "tool_name", "name");
|
|
547
|
+
if (name) line = "Tool: " + name;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
default:
|
|
551
|
+
return ["", false];
|
|
552
|
+
}
|
|
553
|
+
line = line.trim();
|
|
554
|
+
if (!line) return ["", false];
|
|
555
|
+
const runes = [...line];
|
|
556
|
+
if (runes.length > MAX_THINKING_STEP_RUNES) {
|
|
557
|
+
line = runes.slice(0, MAX_THINKING_STEP_RUNES).join("") + "\u2026";
|
|
558
|
+
}
|
|
559
|
+
return [line, true];
|
|
256
560
|
}
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
561
|
+
function formatPlanStepLine(data, suffix) {
|
|
562
|
+
const stepID = strField(data, "step_id");
|
|
563
|
+
const desc = strField(data, "description");
|
|
564
|
+
if (stepID && suffix) return `Step ${stepID}: ${suffix}`;
|
|
565
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
566
|
+
if (stepID) return `Step ${stepID}`;
|
|
567
|
+
if (desc && suffix) return `Step: ${suffix}`;
|
|
568
|
+
if (desc) return `Step: ${desc}`;
|
|
569
|
+
if (suffix) return "Step: " + suffix;
|
|
570
|
+
return "";
|
|
571
|
+
}
|
|
572
|
+
function formatAgentStepLine(data, suffix) {
|
|
573
|
+
const stepID = strField(data, "step_id");
|
|
574
|
+
const desc = strField(data, "description");
|
|
575
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
576
|
+
if (desc) return suffix ? `Step: ${suffix}` : `Step: ${desc}`;
|
|
577
|
+
if (stepID) return `Step ${stepID}`;
|
|
578
|
+
return "";
|
|
579
|
+
}
|
|
580
|
+
function strField(data, ...keys) {
|
|
581
|
+
for (const key of keys) {
|
|
582
|
+
const v = data[key];
|
|
583
|
+
if (typeof v === "string") {
|
|
584
|
+
const s = v.trim();
|
|
585
|
+
if (s) return s;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/appkit/classifier.ts
|
|
592
|
+
var ChatEventTerminal = /* @__PURE__ */ ((ChatEventTerminal2) => {
|
|
593
|
+
ChatEventTerminal2[ChatEventTerminal2["Continue"] = 0] = "Continue";
|
|
594
|
+
ChatEventTerminal2[ChatEventTerminal2["DeliverableComplete"] = 1] = "DeliverableComplete";
|
|
595
|
+
ChatEventTerminal2[ChatEventTerminal2["FailedComplete"] = 2] = "FailedComplete";
|
|
596
|
+
return ChatEventTerminal2;
|
|
597
|
+
})(ChatEventTerminal || {});
|
|
598
|
+
var EVENT_LOOP_HISTORY_REPLAYED = "soothe.lifecycle.loop.history.replayed";
|
|
599
|
+
var EventClassifier = class {
|
|
600
|
+
deliverablePhases;
|
|
601
|
+
minDeliverableRunes;
|
|
602
|
+
thinkingStepEvents;
|
|
603
|
+
treatStatusIdleAsComplete;
|
|
604
|
+
constructor(cfg) {
|
|
605
|
+
if (!cfg.deliverablePhases) {
|
|
606
|
+
throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
|
|
607
|
+
}
|
|
608
|
+
this.deliverablePhases = cfg.deliverablePhases;
|
|
609
|
+
this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
|
|
610
|
+
this.thinkingStepEvents = cfg.thinkingStepEvents;
|
|
611
|
+
this.treatStatusIdleAsComplete = Boolean(cfg.treatStatusIdleAsComplete);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Inspects one decoded event and returns its outcome. `accumulated` is the
|
|
615
|
+
* running assistant text so far, used to pick the final reply when a
|
|
616
|
+
* deliverable event arrives.
|
|
617
|
+
*/
|
|
618
|
+
classify(msg, accumulated) {
|
|
619
|
+
return this.processChatEvent(msg, accumulated);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Reports whether a persisted completion_event is user-facing. Uses the
|
|
623
|
+
* configured deliverable phase set; recognizes the protocol output namespace
|
|
624
|
+
* and final_report component as deliverable.
|
|
625
|
+
*/
|
|
626
|
+
isDeliverableCompletionEvent(eventType) {
|
|
627
|
+
if (!eventType) return false;
|
|
628
|
+
switch (eventType) {
|
|
629
|
+
case "status.idle":
|
|
630
|
+
case "idle_timeout":
|
|
631
|
+
case "query_timeout":
|
|
632
|
+
case "stream_closed":
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
if (eventType === EventFinalReport) return true;
|
|
636
|
+
if (eventType.startsWith("soothe.protocol.message.")) {
|
|
637
|
+
const phase = eventType.slice("soothe.protocol.message.".length);
|
|
638
|
+
return this.isDeliverableLoopPhase(phase);
|
|
639
|
+
}
|
|
640
|
+
return eventType.includes("soothe.output") && eventType.includes("responded");
|
|
641
|
+
}
|
|
642
|
+
isDeliverableLoopPhase(phase) {
|
|
643
|
+
return this.deliverablePhases.has(phase);
|
|
644
|
+
}
|
|
645
|
+
deliverableResult(content, completionEvent) {
|
|
646
|
+
return { content, terminal: 1 /* DeliverableComplete */, completionEvent };
|
|
647
|
+
}
|
|
648
|
+
continueResult(content) {
|
|
649
|
+
return { content, terminal: 0 /* Continue */ };
|
|
650
|
+
}
|
|
651
|
+
failedResult(err) {
|
|
652
|
+
return { terminal: 2 /* FailedComplete */, err };
|
|
653
|
+
}
|
|
654
|
+
/** Reports whether trimmed assistant text is long enough to persist as final. */
|
|
655
|
+
isSubstantiveAssistantReply(content) {
|
|
656
|
+
return [...content.trim()].length >= this.minDeliverableRunes;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Picks the user-visible reply for a completed query. Only a deliverable
|
|
660
|
+
* terminal result with a recognized completion event yields a final reply.
|
|
661
|
+
*/
|
|
662
|
+
resolveDeliverableFinalContent(eventResult, _accumulated) {
|
|
663
|
+
if (eventResult.terminal !== 1 /* DeliverableComplete */) return ["", false];
|
|
664
|
+
if (!this.isDeliverableCompletionEvent(eventResult.completionEvent ?? "")) return ["", false];
|
|
665
|
+
const final = (eventResult.content ?? "").trim();
|
|
666
|
+
if (final) return [final, true];
|
|
667
|
+
return ["", false];
|
|
668
|
+
}
|
|
669
|
+
/** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
|
|
670
|
+
processChatEvent(msg, accumulated) {
|
|
671
|
+
if (!msg || typeof msg !== "object") {
|
|
672
|
+
return { terminal: 0 /* Continue */ };
|
|
673
|
+
}
|
|
674
|
+
const m = msg;
|
|
675
|
+
const typ = m.type;
|
|
676
|
+
if (typ === "next") {
|
|
677
|
+
return this.classifyNextEnvelope(m, accumulated);
|
|
678
|
+
}
|
|
679
|
+
if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack") {
|
|
680
|
+
return { terminal: 0 /* Continue */ };
|
|
681
|
+
}
|
|
682
|
+
if (typ === "status") {
|
|
683
|
+
if (this.treatStatusIdleAsComplete && String(m.state ?? "").trim().toLowerCase() === "idle" && this.isSubstantiveAssistantReply(accumulated)) {
|
|
684
|
+
return this.deliverableResult(accumulated.trim(), "status.idle");
|
|
685
|
+
}
|
|
686
|
+
return { terminal: 0 /* Continue */ };
|
|
687
|
+
}
|
|
688
|
+
if (typ === "error") {
|
|
689
|
+
const errObj = m.error ?? {};
|
|
690
|
+
const code = typeof errObj.code === "number" ? errObj.code : -32603;
|
|
691
|
+
return this.failedResult(
|
|
692
|
+
new DaemonError(code, errObj.message ?? "daemon error", errObj.data)
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (typ === "event") {
|
|
696
|
+
return this.classifyEventPayload(
|
|
697
|
+
m.namespace ?? null,
|
|
698
|
+
m.mode ?? "",
|
|
699
|
+
m.data
|
|
268
700
|
);
|
|
269
701
|
}
|
|
702
|
+
return { terminal: 0 /* Continue */ };
|
|
270
703
|
}
|
|
271
|
-
|
|
704
|
+
/** Classifies a `next` envelope by projecting its payload. */
|
|
705
|
+
classifyNextEnvelope(env, accumulated) {
|
|
706
|
+
const payload = env.payload ?? {};
|
|
707
|
+
const innerData = payload.data;
|
|
708
|
+
if (innerData && typeof innerData === "object") {
|
|
709
|
+
const innerType = innerData.type ?? "";
|
|
710
|
+
if (innerType === "status") {
|
|
711
|
+
return this.processChatEvent(innerData, accumulated);
|
|
712
|
+
}
|
|
713
|
+
const innerMode = innerData.mode ?? "";
|
|
714
|
+
if (innerMode) {
|
|
715
|
+
return this.classifyEventPayload(
|
|
716
|
+
innerData.namespace ?? payload.namespace ?? null,
|
|
717
|
+
innerMode,
|
|
718
|
+
innerData.data
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const mode = payload.mode ?? "";
|
|
723
|
+
if (mode === "status" || mode === "") {
|
|
724
|
+
if (typeof payload.state === "string" || innerData && "state" in (innerData ?? {})) {
|
|
725
|
+
return this.processChatEvent({ type: "status", ...innerData ?? payload }, accumulated);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (mode) {
|
|
729
|
+
return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
|
|
730
|
+
}
|
|
731
|
+
return { terminal: 0 /* Continue */ };
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Classifies an event payload by (namespace, mode, phase). `data` may be a
|
|
735
|
+
* map or an array of messages (mode="messages").
|
|
736
|
+
*/
|
|
737
|
+
classifyEventPayload(namespace, mode, data) {
|
|
738
|
+
const ns = namespaceToString(namespace);
|
|
739
|
+
const dataMap = normalizeEventData(data);
|
|
740
|
+
if (dataMap) {
|
|
741
|
+
let dataType2 = ns;
|
|
742
|
+
const dt2 = dataMap["type"];
|
|
743
|
+
if (typeof dt2 === "string" && dt2) dataType2 = dt2;
|
|
744
|
+
if (dataType2 === EVENT_LOOP_HISTORY_REPLAYED) {
|
|
745
|
+
return { terminal: 0 /* Continue */ };
|
|
746
|
+
}
|
|
747
|
+
const [step, ok] = extractThinkingStep(dataType2, dataMap, this.thinkingStepEvents);
|
|
748
|
+
if (ok) {
|
|
749
|
+
return { thinkingStep: step, terminal: 0 /* Continue */ };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (mode === "messages") {
|
|
753
|
+
const result = this.classifyMessagesMode(data, ns);
|
|
754
|
+
if (result) return result;
|
|
755
|
+
}
|
|
756
|
+
if (!dataMap) {
|
|
757
|
+
return { terminal: 0 /* Continue */ };
|
|
758
|
+
}
|
|
759
|
+
let dataType = ns;
|
|
760
|
+
const dt = dataMap["type"];
|
|
761
|
+
if (typeof dt === "string" && dt) dataType = dt;
|
|
762
|
+
let completionEvent = dataType;
|
|
763
|
+
if (!completionEvent) completionEvent = ns;
|
|
764
|
+
if (isNamespaceMatch(ns, dataType, "soothe.output") || isNamespaceMatch(ns, dataType, "responded")) {
|
|
765
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
766
|
+
if (ok) {
|
|
767
|
+
if (this.isFinalOutputEvent(dataType, ns)) {
|
|
768
|
+
return this.deliverableResult(content, completionEvent);
|
|
769
|
+
}
|
|
770
|
+
return this.continueResult(content);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (isNamespaceMatch(ns, dataType, "agent_loop.completed") || isNamespaceMatch(ns, dataType, "agent_loop.reasoned") || isNamespaceMatch(ns, dataType, "loop.completed")) {
|
|
774
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
775
|
+
if (ok) return this.continueResult(content);
|
|
776
|
+
}
|
|
777
|
+
if (isNamespaceMatch(ns, dataType, "final_report")) {
|
|
778
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
779
|
+
if (ok) return this.deliverableResult(content, completionEvent);
|
|
780
|
+
}
|
|
781
|
+
if (dataType.includes("soothe.error.") || ns.includes("soothe.error.")) {
|
|
782
|
+
const errType = dataType || ns;
|
|
783
|
+
const msg = dataMap["message"];
|
|
784
|
+
if (typeof msg === "string" && msg) {
|
|
785
|
+
return this.failedResult(new Error(`${errType}: ${msg}`));
|
|
786
|
+
}
|
|
787
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
788
|
+
if (ok) return this.failedResult(new Error(`${errType}: ${content}`));
|
|
789
|
+
return this.failedResult(new Error(errType));
|
|
790
|
+
}
|
|
791
|
+
if (isNamespaceMatch(ns, dataType, "stream") || isNamespaceMatch(ns, dataType, "progress") || isNamespaceMatch(ns, dataType, "tool_call_updates_batch") || isNamespaceMatch(ns, dataType, "soothe.stream.tool_call.update")) {
|
|
792
|
+
const delta = dataMap["delta"];
|
|
793
|
+
if (typeof delta === "string") return this.continueResult(delta);
|
|
794
|
+
}
|
|
795
|
+
if (isNamespaceMatch(ns, dataType, "heartbeat") || isNamespaceMatch(ns, dataType, "system.daemon") || isNamespaceMatch(ns, dataType, "agent_loop.started") || isNamespaceMatch(ns, dataType, "intent.classified")) {
|
|
796
|
+
return { terminal: 0 /* Continue */ };
|
|
797
|
+
}
|
|
798
|
+
return { terminal: 0 /* Continue */ };
|
|
799
|
+
}
|
|
800
|
+
/** Classifies a mode="messages" payload (array of message objects). */
|
|
801
|
+
classifyMessagesMode(data, _ns) {
|
|
802
|
+
const items = Array.isArray(data) ? data : null;
|
|
803
|
+
if (!items || items.length === 0) return null;
|
|
804
|
+
const first = items[0];
|
|
805
|
+
if (!first || typeof first !== "object") return null;
|
|
806
|
+
const [msgType, rawContent, phase, hasPayload] = firstMessagePayload(data);
|
|
807
|
+
if (hasPayload && rawContent && isStreamingMessageType(msgType)) {
|
|
808
|
+
return this.continueResult(rawContent);
|
|
809
|
+
}
|
|
810
|
+
const loopMsg = loopAIMessage(data);
|
|
811
|
+
if (loopMsg) {
|
|
812
|
+
const content = loopMsg.content;
|
|
813
|
+
if (content) {
|
|
814
|
+
if (isStreamingMessageType(loopMsg.type)) {
|
|
815
|
+
return this.continueResult(content);
|
|
816
|
+
}
|
|
817
|
+
if (this.isDeliverableLoopPhase(loopMsg.phase) && this.isSubstantiveAssistantReply(content)) {
|
|
818
|
+
return this.deliverableResult(content, "soothe.protocol.message." + loopMsg.phase);
|
|
819
|
+
}
|
|
820
|
+
return this.continueResult(content);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const [directContent, directOk] = this.messagesModeAssistantContent(data);
|
|
824
|
+
if (directOk && this.isSubstantiveAssistantReply(directContent)) {
|
|
825
|
+
return this.deliverableResult(directContent, "soothe.protocol.message.direct_model");
|
|
826
|
+
}
|
|
827
|
+
if (hasPayload && rawContent) {
|
|
828
|
+
if (isTerminalMessageType(msgType) || msgType === "") {
|
|
829
|
+
if (this.isDeliverableLoopPhase(phase) && this.isSubstantiveAssistantReply(rawContent)) {
|
|
830
|
+
return this.deliverableResult(rawContent, "soothe.protocol.message." + phase);
|
|
831
|
+
}
|
|
832
|
+
return this.continueResult(rawContent);
|
|
833
|
+
}
|
|
834
|
+
return this.continueResult(rawContent);
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Extracts plain assistant text from mode="messages" events that carry a
|
|
840
|
+
* terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
|
|
841
|
+
* before phase tagging; prefer deliverablePhases including text_completion).
|
|
842
|
+
*/
|
|
843
|
+
messagesModeAssistantContent(data) {
|
|
844
|
+
if (!Array.isArray(data) || data.length === 0) return ["", false];
|
|
845
|
+
const msgMap = data[0];
|
|
846
|
+
if (!msgMap || typeof msgMap !== "object") return ["", false];
|
|
847
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
848
|
+
if (phase) return ["", false];
|
|
849
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
850
|
+
if (msgType && !isTerminalMessageType(msgType)) return ["", false];
|
|
851
|
+
const content = extractContentFromMessage(msgMap).trim();
|
|
852
|
+
if (!content) return ["", false];
|
|
853
|
+
return [content, true];
|
|
854
|
+
}
|
|
855
|
+
/** soothe output/responded events that carry user-facing final text. */
|
|
856
|
+
isFinalOutputEvent(dataType, ns) {
|
|
857
|
+
const combined = dataType + " " + ns;
|
|
858
|
+
if (combined.includes("final_report")) return true;
|
|
859
|
+
for (const phase of this.deliverablePhases) {
|
|
860
|
+
if (combined.includes(phase)) return true;
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
function isStreamingMessageType(msgType) {
|
|
866
|
+
return msgType === "AIMessageChunk" || msgType === "ai_chunk" || msgType === "message_chunk";
|
|
272
867
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
868
|
+
function isTerminalMessageType(msgType) {
|
|
869
|
+
return msgType === "AIMessage" || msgType === "ai" || msgType === "assistant";
|
|
870
|
+
}
|
|
871
|
+
function firstMessagePayload(data) {
|
|
872
|
+
if (!Array.isArray(data) || data.length === 0) return ["", "", "", false];
|
|
873
|
+
const msgMap = data[0];
|
|
874
|
+
if (!msgMap || typeof msgMap !== "object") return ["", "", "", false];
|
|
875
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
876
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase : "";
|
|
877
|
+
const content = extractContentFromMessage(msgMap);
|
|
878
|
+
return [msgType, content, phase, true];
|
|
879
|
+
}
|
|
880
|
+
function loopAIMessage(data) {
|
|
881
|
+
if (!Array.isArray(data) || data.length === 0) return null;
|
|
882
|
+
const msgMap = data[0];
|
|
883
|
+
if (!msgMap || typeof msgMap !== "object") return null;
|
|
884
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
885
|
+
if (!phase) return null;
|
|
886
|
+
const type = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
887
|
+
const content = extractContentFromMessage(msgMap);
|
|
888
|
+
return { type, content, phase };
|
|
889
|
+
}
|
|
890
|
+
function extractContentFromMessage(msgMap) {
|
|
891
|
+
const c = msgMap.content;
|
|
892
|
+
if (typeof c === "string" && c) return c;
|
|
893
|
+
if (Array.isArray(c) && c.length > 0) {
|
|
894
|
+
let b = "";
|
|
895
|
+
for (const item of c) {
|
|
896
|
+
if (typeof item === "string") {
|
|
897
|
+
b += item;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (item && typeof item === "object") {
|
|
901
|
+
const blk = item;
|
|
902
|
+
const t = blk.text;
|
|
903
|
+
if (typeof t === "string") b += t;
|
|
904
|
+
}
|
|
283
905
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
906
|
+
return b;
|
|
907
|
+
}
|
|
908
|
+
const blocks = msgMap.content_blocks;
|
|
909
|
+
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
910
|
+
let b = "";
|
|
911
|
+
for (const blk of blocks) {
|
|
912
|
+
if (blk && typeof blk === "object") {
|
|
913
|
+
const m = blk;
|
|
914
|
+
const t = m.text;
|
|
915
|
+
if (typeof t === "string") b += t;
|
|
289
916
|
}
|
|
290
917
|
}
|
|
918
|
+
return b;
|
|
291
919
|
}
|
|
292
|
-
|
|
920
|
+
return "";
|
|
293
921
|
}
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
922
|
+
function extractContentFromData(data) {
|
|
923
|
+
for (const key of [
|
|
924
|
+
"final_stdout_message",
|
|
925
|
+
"completion_summary",
|
|
926
|
+
"content",
|
|
927
|
+
"text",
|
|
928
|
+
"response",
|
|
929
|
+
"output",
|
|
930
|
+
"message",
|
|
931
|
+
"report"
|
|
932
|
+
]) {
|
|
933
|
+
const val = data[key];
|
|
934
|
+
if (typeof val === "string" && val) return [val, true];
|
|
935
|
+
}
|
|
936
|
+
const nested = data.data;
|
|
937
|
+
if (nested && typeof nested === "object") {
|
|
938
|
+
const nm = nested;
|
|
939
|
+
for (const key of [
|
|
940
|
+
"final_stdout_message",
|
|
941
|
+
"completion_summary",
|
|
942
|
+
"content",
|
|
943
|
+
"text",
|
|
944
|
+
"response",
|
|
945
|
+
"output",
|
|
946
|
+
"message",
|
|
947
|
+
"report"
|
|
948
|
+
]) {
|
|
949
|
+
const val = nm[key];
|
|
950
|
+
if (typeof val === "string" && val) return [val, true];
|
|
303
951
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
952
|
+
}
|
|
953
|
+
return ["", false];
|
|
954
|
+
}
|
|
955
|
+
function isNamespaceMatch(ns, dataType, pattern) {
|
|
956
|
+
return dataType.includes(pattern) || ns.includes(pattern);
|
|
957
|
+
}
|
|
958
|
+
function namespaceToString(namespace) {
|
|
959
|
+
if (typeof namespace === "string") return namespace;
|
|
960
|
+
if (Array.isArray(namespace)) return namespace.filter((s) => typeof s === "string").join(".");
|
|
961
|
+
return "";
|
|
962
|
+
}
|
|
963
|
+
function normalizeEventData(data) {
|
|
964
|
+
if (data == null) return null;
|
|
965
|
+
if (typeof data === "object" && !Array.isArray(data)) {
|
|
966
|
+
return data;
|
|
967
|
+
}
|
|
968
|
+
if (typeof data === "string") {
|
|
969
|
+
try {
|
|
970
|
+
const m = JSON.parse(data);
|
|
971
|
+
if (m && typeof m === "object" && !Array.isArray(m)) return m;
|
|
972
|
+
} catch {
|
|
973
|
+
return null;
|
|
307
974
|
}
|
|
308
975
|
}
|
|
309
|
-
|
|
976
|
+
return null;
|
|
310
977
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
978
|
+
|
|
979
|
+
// src/appkit/query_gate.ts
|
|
980
|
+
var ErrQueryBusy = class extends Error {
|
|
981
|
+
constructor() {
|
|
982
|
+
super("appkit: query already in progress for session");
|
|
983
|
+
this.name = "ErrQueryBusy";
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
var QueryGate = class {
|
|
987
|
+
active = /* @__PURE__ */ new Map();
|
|
988
|
+
/** Constructs an empty gate. */
|
|
989
|
+
constructor() {
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
|
|
993
|
+
* already in flight. `abort` is the AbortController for the query's timeout
|
|
994
|
+
* context. `sendCancel` is the daemon-cancel sender; it is invoked from
|
|
995
|
+
* `cancel()` on a detached 10s timeout.
|
|
996
|
+
*/
|
|
997
|
+
acquire(sessionID, abort, sendCancel) {
|
|
998
|
+
if (this.active.has(sessionID)) {
|
|
999
|
+
throw new ErrQueryBusy();
|
|
1000
|
+
}
|
|
1001
|
+
this.active.set(sessionID, { abort, sendCancel });
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Cooperatively stops a running query for sessionID. Sends the daemon cancel
|
|
1005
|
+
* (on a detached 10s-timeout abort so caller cancellation cannot block the
|
|
1006
|
+
* wire send) BEFORE aborting the local context. Returns silently if no query
|
|
1007
|
+
* is in flight (intent already satisfied).
|
|
1008
|
+
*/
|
|
1009
|
+
async cancel(sessionID) {
|
|
1010
|
+
const state = this.active.get(sessionID);
|
|
1011
|
+
if (!state) return;
|
|
1012
|
+
this.active.delete(sessionID);
|
|
1013
|
+
if (state.sendCancel) {
|
|
1014
|
+
const detached = new AbortController();
|
|
1015
|
+
const timer = setTimeout(() => detached.abort(), 1e4);
|
|
1016
|
+
try {
|
|
1017
|
+
await state.sendCancel(detached.signal);
|
|
1018
|
+
} catch {
|
|
1019
|
+
} finally {
|
|
1020
|
+
clearTimeout(timer);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
state.abort.abort();
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Clears the gate for sessionID without sending a daemon cancel. Call when a
|
|
1027
|
+
* query completes normally (success or local failure) so the next turn can
|
|
1028
|
+
* acquire.
|
|
1029
|
+
*/
|
|
1030
|
+
release(sessionID) {
|
|
1031
|
+
this.active.delete(sessionID);
|
|
1032
|
+
}
|
|
1033
|
+
/** Reports whether a query is in flight for sessionID. */
|
|
1034
|
+
isActive(sessionID) {
|
|
1035
|
+
return this.active.has(sessionID);
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// src/appkit/client.ts
|
|
1040
|
+
function defaultClientFactory() {
|
|
1041
|
+
return (url, config) => {
|
|
1042
|
+
return new Client(url, config ?? defaultConfig());
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function defaultBootstrapFunc() {
|
|
1046
|
+
return async (client, workspaceID, userID, config) => {
|
|
1047
|
+
const c = client;
|
|
1048
|
+
const opts = {
|
|
1049
|
+
client_workspace: workspaceID,
|
|
1050
|
+
user_id: userID,
|
|
1051
|
+
client_workspace_id: workspaceID
|
|
1052
|
+
};
|
|
1053
|
+
return bootstrapLoopSession(c, "", config, opts);
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/appkit/pool.ts
|
|
1058
|
+
var ErrPoolExhausted = class extends Error {
|
|
1059
|
+
constructor() {
|
|
1060
|
+
super("appkit: connection pool exhausted");
|
|
1061
|
+
this.name = "ErrPoolExhausted";
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
function defaultPoolConfig() {
|
|
1065
|
+
return {
|
|
1066
|
+
poolSize: 1e3,
|
|
1067
|
+
queryTimeout: 30 * 60 * 1e3,
|
|
1068
|
+
connectionTimeout: 3e4,
|
|
1069
|
+
maxIdleTime: 10 * 60 * 1e3,
|
|
1070
|
+
healthCheckInterval: 3e4
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
var PooledConn = class {
|
|
1074
|
+
slotID;
|
|
1075
|
+
client;
|
|
1076
|
+
eventStream = null;
|
|
1077
|
+
streamController = null;
|
|
1078
|
+
sessionID = "";
|
|
1079
|
+
loopID = "";
|
|
1080
|
+
workspaceID = "";
|
|
1081
|
+
lastUsed = 0;
|
|
1082
|
+
constructor(slotID, client) {
|
|
1083
|
+
this.slotID = slotID;
|
|
1084
|
+
this.client = client;
|
|
1085
|
+
}
|
|
1086
|
+
/** Reports whether the underlying client signalled a drop. */
|
|
1087
|
+
isDisconnected() {
|
|
1088
|
+
return this.client.isDisconnected();
|
|
1089
|
+
}
|
|
1090
|
+
isConnected() {
|
|
1091
|
+
return this.client.isConnected() && !this.isDisconnected();
|
|
1092
|
+
}
|
|
1093
|
+
getLoopID() {
|
|
1094
|
+
return this.loopID;
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
var ConnectionPool = class {
|
|
1098
|
+
cfg;
|
|
1099
|
+
scfg;
|
|
1100
|
+
factory;
|
|
1101
|
+
bootstrap;
|
|
1102
|
+
store;
|
|
1103
|
+
pool = [];
|
|
1104
|
+
activeSlots = /* @__PURE__ */ new Map();
|
|
1105
|
+
nextSlotID = 1;
|
|
1106
|
+
url;
|
|
1107
|
+
/**
|
|
1108
|
+
* Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
|
|
1109
|
+
* defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
|
|
1110
|
+
* factory/bootstrap fall back to the defaults.
|
|
1111
|
+
*/
|
|
1112
|
+
constructor(url, store, cfg, scfg, factory) {
|
|
1113
|
+
this.cfg = cfg ?? defaultPoolConfig();
|
|
1114
|
+
this.scfg = scfg ?? defaultConfig();
|
|
1115
|
+
this.factory = factory ?? defaultClientFactory();
|
|
1116
|
+
this.bootstrap = defaultBootstrapFunc();
|
|
1117
|
+
this.store = store;
|
|
1118
|
+
this.url = url;
|
|
1119
|
+
for (let i = 0; i < this.cfg.poolSize; i++) {
|
|
1120
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(url, this.scfg)));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
/** Overrides the loop bootstrap function (useful for test fakes). */
|
|
1124
|
+
withBootstrap(f) {
|
|
1125
|
+
if (f) this.bootstrap = f;
|
|
1126
|
+
return this;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Returns a live connection for sessionID, reusing an active slot or
|
|
1130
|
+
* bootstrapping/reattaching as needed. The caller must call `release()`
|
|
1131
|
+
* when done with the connection (a turn completes or the session is reset).
|
|
1132
|
+
*/
|
|
1133
|
+
async acquire(sessionID, workspaceID, userID, _signal) {
|
|
1134
|
+
const existing = this.activeSlots.get(sessionID);
|
|
1135
|
+
if (existing) {
|
|
1136
|
+
if (existing.isDisconnected() || !existing.isConnected()) {
|
|
1137
|
+
await this.release(sessionID);
|
|
1138
|
+
} else {
|
|
1139
|
+
const idleTooLong = this.cfg.maxIdleTime > 0 && existing.lastUsed > 0 && Date.now() - existing.lastUsed > this.cfg.maxIdleTime;
|
|
1140
|
+
if (idleTooLong) {
|
|
1141
|
+
await this.release(sessionID);
|
|
1142
|
+
} else {
|
|
1143
|
+
existing.lastUsed = Date.now();
|
|
1144
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
1145
|
+
});
|
|
1146
|
+
return existing;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const conn = this.pool.pop();
|
|
1151
|
+
if (!conn) throw new ErrPoolExhausted();
|
|
1152
|
+
this.activeSlots.set(sessionID, conn);
|
|
1153
|
+
const { loopID, ok } = await this.store.getLoopIDForSession(sessionID).catch(() => ({ loopID: "", ok: false }));
|
|
1154
|
+
let finalLoopID = "";
|
|
316
1155
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
1156
|
+
if (!ok || !loopID) {
|
|
1157
|
+
await conn.client.connect();
|
|
1158
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
1159
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
1160
|
+
});
|
|
1161
|
+
} else {
|
|
1162
|
+
try {
|
|
1163
|
+
await this.resumeAndReattach(conn, loopID);
|
|
1164
|
+
finalLoopID = loopID;
|
|
1165
|
+
} catch {
|
|
1166
|
+
await conn.client.connect();
|
|
1167
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
1168
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
319
1172
|
} catch (err) {
|
|
320
|
-
|
|
1173
|
+
await this.release(sessionID);
|
|
1174
|
+
throw err;
|
|
321
1175
|
}
|
|
322
|
-
|
|
1176
|
+
conn.sessionID = sessionID;
|
|
1177
|
+
conn.loopID = finalLoopID;
|
|
1178
|
+
conn.workspaceID = workspaceID;
|
|
1179
|
+
conn.lastUsed = Date.now();
|
|
1180
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
1181
|
+
});
|
|
1182
|
+
return conn;
|
|
1183
|
+
}
|
|
1184
|
+
/** Tears down the connection for sessionID and returns the slot. */
|
|
1185
|
+
async release(sessionID) {
|
|
1186
|
+
const conn = this.activeSlots.get(sessionID);
|
|
1187
|
+
if (!conn) return;
|
|
1188
|
+
this.activeSlots.delete(sessionID);
|
|
1189
|
+
if (conn.streamController) {
|
|
1190
|
+
conn.streamController.abort();
|
|
1191
|
+
conn.streamController = null;
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
conn.client.close();
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
conn.sessionID = "";
|
|
1198
|
+
conn.loopID = "";
|
|
1199
|
+
conn.eventStream = null;
|
|
1200
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(this.url, this.scfg)));
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Tears down the connection for sessionID so the next acquire bootstraps
|
|
1204
|
+
* fresh. The store should archive the loop id so getLoopIDForSession returns
|
|
1205
|
+
* false next time.
|
|
1206
|
+
*/
|
|
1207
|
+
async resetSession(sessionID) {
|
|
1208
|
+
await this.release(sessionID);
|
|
1209
|
+
}
|
|
1210
|
+
/** Gracefully shuts down all active connections. */
|
|
1211
|
+
stop() {
|
|
1212
|
+
for (const [sid, conn] of this.activeSlots) {
|
|
1213
|
+
if (conn.streamController) conn.streamController.abort();
|
|
1214
|
+
try {
|
|
1215
|
+
conn.client.close();
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
this.activeSlots.delete(sid);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/** Stats snapshot for observability. */
|
|
1222
|
+
stats() {
|
|
1223
|
+
return { active: this.activeSlots.size, idle: this.pool.length };
|
|
1224
|
+
}
|
|
1225
|
+
/** Bootstrap a fresh loop and start the reader. */
|
|
1226
|
+
async bootstrapNew(conn, workspaceID, userID) {
|
|
1227
|
+
const loopID = await this.bootstrap(conn.client, workspaceID, userID, this.scfg);
|
|
1228
|
+
this.startReader(conn);
|
|
1229
|
+
return loopID;
|
|
1230
|
+
}
|
|
1231
|
+
/** Reconnect + reattach an existing loop, then start the reader. */
|
|
1232
|
+
async resumeAndReattach(conn, loopID) {
|
|
1233
|
+
await conn.client.connect();
|
|
1234
|
+
try {
|
|
1235
|
+
await conn.client.reattachAndProbe(loopID);
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
if (err instanceof StaleLoopError) throw err;
|
|
1238
|
+
throw err;
|
|
1239
|
+
}
|
|
1240
|
+
this.startReader(conn);
|
|
323
1241
|
}
|
|
324
|
-
|
|
1242
|
+
/** Starts a receiveMessages generator and stores the stream + controller. */
|
|
1243
|
+
startReader(conn) {
|
|
1244
|
+
const controller = new AbortController();
|
|
1245
|
+
conn.streamController = controller;
|
|
1246
|
+
conn.eventStream = conn.client.receiveMessages(controller.signal);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// src/appkit/attachments.ts
|
|
1251
|
+
function compactDefaults(opts) {
|
|
1252
|
+
return {
|
|
1253
|
+
maxDim: opts?.maxDim && opts.maxDim > 0 ? opts.maxDim : 768,
|
|
1254
|
+
quality: opts?.jpegQuality && opts.jpegQuality > 0 ? opts.jpegQuality : 85
|
|
1255
|
+
};
|
|
325
1256
|
}
|
|
1257
|
+
var sharpLoader = null;
|
|
1258
|
+
async function loadSharp() {
|
|
1259
|
+
if (!sharpLoader) {
|
|
1260
|
+
sharpLoader = (async () => {
|
|
1261
|
+
try {
|
|
1262
|
+
const m = await Function('return import("sharp")')();
|
|
1263
|
+
return m;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
})();
|
|
1268
|
+
}
|
|
1269
|
+
return sharpLoader;
|
|
1270
|
+
}
|
|
1271
|
+
async function compactImageAttachment(mimeType, dataB64, opts) {
|
|
1272
|
+
if (!dataB64 || !mimeType.startsWith("image/")) {
|
|
1273
|
+
return [mimeType, dataB64];
|
|
1274
|
+
}
|
|
1275
|
+
let raw;
|
|
1276
|
+
try {
|
|
1277
|
+
raw = Buffer.from(dataB64, "base64");
|
|
1278
|
+
} catch {
|
|
1279
|
+
return [mimeType, dataB64];
|
|
1280
|
+
}
|
|
1281
|
+
if (raw.length === 0) return [mimeType, dataB64];
|
|
1282
|
+
const sharpMod = await loadSharp();
|
|
1283
|
+
if (!sharpMod) return [mimeType, dataB64];
|
|
1284
|
+
const { maxDim, quality } = compactDefaults(opts);
|
|
1285
|
+
try {
|
|
1286
|
+
const img = sharpMod.default(raw, { failOn: "none" });
|
|
1287
|
+
const meta = await img.metadata();
|
|
1288
|
+
const w = meta.width ?? 0;
|
|
1289
|
+
const h = meta.height ?? 0;
|
|
1290
|
+
if (w <= 0 || h <= 0 || w <= maxDim && h <= maxDim) {
|
|
1291
|
+
return [mimeType, dataB64];
|
|
1292
|
+
}
|
|
1293
|
+
let nw = w;
|
|
1294
|
+
let nh = h;
|
|
1295
|
+
if (w >= h) {
|
|
1296
|
+
if (w > maxDim) {
|
|
1297
|
+
nw = maxDim;
|
|
1298
|
+
nh = Math.max(1, Math.round(h * maxDim / w));
|
|
1299
|
+
}
|
|
1300
|
+
} else if (h > maxDim) {
|
|
1301
|
+
nh = maxDim;
|
|
1302
|
+
nw = Math.max(1, Math.round(w * maxDim / h));
|
|
1303
|
+
}
|
|
1304
|
+
const resized = img.resize(nw, nh, { fit: "fill" });
|
|
1305
|
+
if (mimeType === "image/png") {
|
|
1306
|
+
const buf2 = await resized.png().toBuffer();
|
|
1307
|
+
return [mimeType, buf2.toString("base64")];
|
|
1308
|
+
}
|
|
1309
|
+
const buf = await resized.jpeg({ quality }).toBuffer();
|
|
1310
|
+
return ["image/jpeg", buf.toString("base64")];
|
|
1311
|
+
} catch {
|
|
1312
|
+
return [mimeType, dataB64];
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async function compactAttachments(atts, opts) {
|
|
1316
|
+
if (!atts.length) return atts;
|
|
1317
|
+
const out = [];
|
|
1318
|
+
for (const att of atts) {
|
|
1319
|
+
const cp = { ...att };
|
|
1320
|
+
const mime = typeof cp.mime_type === "string" ? cp.mime_type : "";
|
|
1321
|
+
const data = typeof cp.data === "string" ? cp.data : "";
|
|
1322
|
+
if (mime && data) {
|
|
1323
|
+
const [outMime, outData] = await compactImageAttachment(mime, data, opts);
|
|
1324
|
+
cp.mime_type = outMime;
|
|
1325
|
+
cp.data = outData;
|
|
1326
|
+
}
|
|
1327
|
+
out.push(cp);
|
|
1328
|
+
}
|
|
1329
|
+
return out;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/appkit/turn_runner.ts
|
|
1333
|
+
var ErrQueryTimeout = class extends Error {
|
|
1334
|
+
constructor() {
|
|
1335
|
+
super("appkit: query timeout");
|
|
1336
|
+
this.name = "ErrQueryTimeout";
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
var ErrIdleTimeout = class extends Error {
|
|
1340
|
+
constructor() {
|
|
1341
|
+
super("appkit: idle timeout");
|
|
1342
|
+
this.name = "ErrIdleTimeout";
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
var TimeoutPolicy = /* @__PURE__ */ ((TimeoutPolicy2) => {
|
|
1346
|
+
TimeoutPolicy2[TimeoutPolicy2["Fail"] = 0] = "Fail";
|
|
1347
|
+
TimeoutPolicy2[TimeoutPolicy2["SoftComplete"] = 1] = "SoftComplete";
|
|
1348
|
+
return TimeoutPolicy2;
|
|
1349
|
+
})(TimeoutPolicy || {});
|
|
1350
|
+
var StreamCloseFail = 0 /* Fail */;
|
|
1351
|
+
var StreamCloseSoftComplete = 1 /* SoftComplete */;
|
|
1352
|
+
function inputMessageForLoop(text, loopID, attachments, opts) {
|
|
1353
|
+
const msg = { type: "loop_input", content: text };
|
|
1354
|
+
if (loopID) msg.loop_id = loopID;
|
|
1355
|
+
if (attachments && attachments.length > 0) msg.attachments = attachments;
|
|
1356
|
+
if (opts) {
|
|
1357
|
+
if (opts.intentHint?.trim()) {
|
|
1358
|
+
const hintError = validateLoopInputIntentHint(opts.intentHint);
|
|
1359
|
+
if (hintError) {
|
|
1360
|
+
throw new Error(hintError);
|
|
1361
|
+
}
|
|
1362
|
+
msg.intent_hint = opts.intentHint.trim();
|
|
1363
|
+
}
|
|
1364
|
+
if (opts.preferredSubagent?.trim()) msg.preferred_subagent = opts.preferredSubagent.trim();
|
|
1365
|
+
if (opts.responseSchema && Object.keys(opts.responseSchema).length > 0) {
|
|
1366
|
+
msg.response_schema = opts.responseSchema;
|
|
1367
|
+
}
|
|
1368
|
+
if (opts.responseSchemaName?.trim()) msg.response_schema_name = opts.responseSchemaName.trim();
|
|
1369
|
+
if (opts.responseSchemaStrict !== void 0)
|
|
1370
|
+
msg.response_schema_strict = opts.responseSchemaStrict;
|
|
1371
|
+
}
|
|
1372
|
+
return msg;
|
|
1373
|
+
}
|
|
1374
|
+
function idleTimeoutForTurn(cfg, hasAttachments) {
|
|
1375
|
+
const idle = cfg.idleTimeout ?? 0;
|
|
1376
|
+
if (idle <= 0) return 0;
|
|
1377
|
+
const floor = cfg.minIdleTimeoutWithAttachments ?? 0;
|
|
1378
|
+
if (hasAttachments && floor > 0 && idle < floor) return floor;
|
|
1379
|
+
return idle;
|
|
1380
|
+
}
|
|
1381
|
+
var TurnRunner = class {
|
|
1382
|
+
pool;
|
|
1383
|
+
gate;
|
|
1384
|
+
classifier;
|
|
1385
|
+
store;
|
|
1386
|
+
broadcaster;
|
|
1387
|
+
cfg;
|
|
1388
|
+
buildInput = inputMessageForLoop;
|
|
1389
|
+
onComplete = null;
|
|
1390
|
+
onError = null;
|
|
1391
|
+
constructor(pool, gate, classifier, store, broadcaster, cfg) {
|
|
1392
|
+
this.pool = pool;
|
|
1393
|
+
this.gate = gate;
|
|
1394
|
+
this.classifier = classifier;
|
|
1395
|
+
this.store = store;
|
|
1396
|
+
this.broadcaster = broadcaster;
|
|
1397
|
+
this.cfg = {
|
|
1398
|
+
...cfg,
|
|
1399
|
+
queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
withInputBuilder(f) {
|
|
1403
|
+
if (f) this.buildInput = f;
|
|
1404
|
+
return this;
|
|
1405
|
+
}
|
|
1406
|
+
withOnComplete(f) {
|
|
1407
|
+
this.onComplete = f;
|
|
1408
|
+
return this;
|
|
1409
|
+
}
|
|
1410
|
+
withOnError(f) {
|
|
1411
|
+
this.onError = f;
|
|
1412
|
+
return this;
|
|
1413
|
+
}
|
|
1414
|
+
async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
|
|
1415
|
+
let conn;
|
|
1416
|
+
try {
|
|
1417
|
+
conn = await this.pool.acquire(sessionID, workspaceID, userID, signal);
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
await this.persistFailed(sessionID, "", err);
|
|
1420
|
+
this.broadcastError(sessionID, err);
|
|
1421
|
+
this.onError?.(sessionID, "", err);
|
|
1422
|
+
throw err;
|
|
1423
|
+
}
|
|
1424
|
+
const loopID = conn.getLoopID();
|
|
1425
|
+
const timeoutController = new AbortController();
|
|
1426
|
+
const timeoutMs = this.cfg.queryTimeout;
|
|
1427
|
+
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
1428
|
+
const sendCancel = async (detachedSignal) => {
|
|
1429
|
+
await this.sendLoopCancel(detachedSignal, conn, loopID);
|
|
1430
|
+
};
|
|
1431
|
+
try {
|
|
1432
|
+
this.gate.acquire(sessionID, timeoutController, sendCancel);
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
clearTimeout(timer);
|
|
1435
|
+
await this.pool.release(sessionID);
|
|
1436
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1437
|
+
this.broadcastError(sessionID, err);
|
|
1438
|
+
this.onError?.(sessionID, loopID, err);
|
|
1439
|
+
throw err;
|
|
1440
|
+
}
|
|
1441
|
+
let idleTimer = null;
|
|
1442
|
+
const clearIdle = () => {
|
|
1443
|
+
if (idleTimer) {
|
|
1444
|
+
clearTimeout(idleTimer);
|
|
1445
|
+
idleTimer = null;
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
try {
|
|
1449
|
+
let atts = attachments ?? void 0;
|
|
1450
|
+
if (this.cfg.compactAttachmentsBeforeSend && atts && atts.length > 0) {
|
|
1451
|
+
atts = await compactAttachments(atts, this.cfg.compactImageOpts);
|
|
1452
|
+
}
|
|
1453
|
+
const inputMsg = this.buildInput(message, loopID, atts, opts ?? void 0);
|
|
1454
|
+
try {
|
|
1455
|
+
await conn.client.sendMessage(inputMsg);
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1458
|
+
this.broadcastError(sessionID, err);
|
|
1459
|
+
this.onError?.(sessionID, loopID, err);
|
|
1460
|
+
throw err;
|
|
1461
|
+
}
|
|
1462
|
+
const eventStream = conn.eventStream;
|
|
1463
|
+
if (!eventStream) {
|
|
1464
|
+
const err = new Error(`missing event stream for session ${sessionID} (loop ${loopID})`);
|
|
1465
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1466
|
+
this.broadcastError(sessionID, err);
|
|
1467
|
+
this.onError?.(sessionID, loopID, err);
|
|
1468
|
+
throw err;
|
|
1469
|
+
}
|
|
1470
|
+
let assistantContent = "";
|
|
1471
|
+
const startedAt = Date.now();
|
|
1472
|
+
const idleForTurn = idleTimeoutForTurn(this.cfg, (attachments?.length ?? 0) > 0);
|
|
1473
|
+
let idleReject = null;
|
|
1474
|
+
const armIdle = () => {
|
|
1475
|
+
clearIdle();
|
|
1476
|
+
idleReject = null;
|
|
1477
|
+
if (idleForTurn <= 0) {
|
|
1478
|
+
return new Promise(() => {
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
return new Promise((resolve) => {
|
|
1482
|
+
idleReject = () => resolve("idle");
|
|
1483
|
+
idleTimer = setTimeout(() => {
|
|
1484
|
+
idleReject?.();
|
|
1485
|
+
}, idleForTurn);
|
|
1486
|
+
});
|
|
1487
|
+
};
|
|
1488
|
+
let idleRace = armIdle();
|
|
1489
|
+
const abortRace = new Promise((resolve) => {
|
|
1490
|
+
const onTimeout = () => resolve("timeout");
|
|
1491
|
+
timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
|
|
1492
|
+
if (signal) {
|
|
1493
|
+
const onCaller = () => resolve("caller");
|
|
1494
|
+
signal.addEventListener("abort", onCaller, { once: true });
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
const iterator = eventStream[Symbol.asyncIterator]();
|
|
1498
|
+
while (true) {
|
|
1499
|
+
const next = iterator.next();
|
|
1500
|
+
const raced = await Promise.race([
|
|
1501
|
+
next.then((res2) => ({ tag: "msg", res: res2 })),
|
|
1502
|
+
abortRace.then((tag) => ({ tag })),
|
|
1503
|
+
idleRace.then((tag) => ({ tag }))
|
|
1504
|
+
]);
|
|
1505
|
+
if ("tag" in raced && raced.tag !== "msg") {
|
|
1506
|
+
if (raced.tag === "caller" || signal?.aborted) {
|
|
1507
|
+
clearIdle();
|
|
1508
|
+
const err = new Error("aborted");
|
|
1509
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1510
|
+
this.broadcastError(sessionID, err);
|
|
1511
|
+
this.onError?.(sessionID, loopID, err);
|
|
1512
|
+
throw err;
|
|
1513
|
+
}
|
|
1514
|
+
if (raced.tag === "idle") {
|
|
1515
|
+
clearIdle();
|
|
1516
|
+
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
1517
|
+
});
|
|
1518
|
+
await this.finishTimeout(
|
|
1519
|
+
sessionID,
|
|
1520
|
+
loopID,
|
|
1521
|
+
assistantContent,
|
|
1522
|
+
startedAt,
|
|
1523
|
+
new ErrIdleTimeout(),
|
|
1524
|
+
"idle_timeout",
|
|
1525
|
+
this.cfg.onIdleTimeout ?? 0 /* Fail */
|
|
1526
|
+
);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
clearIdle();
|
|
1530
|
+
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
1531
|
+
});
|
|
1532
|
+
await this.finishTimeout(
|
|
1533
|
+
sessionID,
|
|
1534
|
+
loopID,
|
|
1535
|
+
assistantContent,
|
|
1536
|
+
startedAt,
|
|
1537
|
+
new ErrQueryTimeout(),
|
|
1538
|
+
"query_timeout",
|
|
1539
|
+
this.cfg.onQueryTimeout ?? 0 /* Fail */
|
|
1540
|
+
);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const res = raced.res;
|
|
1544
|
+
if (res.done) {
|
|
1545
|
+
clearIdle();
|
|
1546
|
+
if ((this.cfg.onStreamClose ?? 0 /* Fail */) === 1 /* SoftComplete */ && assistantContent.trim() !== "") {
|
|
1547
|
+
await this.completeTurn(
|
|
1548
|
+
sessionID,
|
|
1549
|
+
loopID,
|
|
1550
|
+
assistantContent,
|
|
1551
|
+
startedAt,
|
|
1552
|
+
"stream_closed"
|
|
1553
|
+
);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const err = new Error("event stream closed");
|
|
1557
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1558
|
+
this.broadcastError(sessionID, err);
|
|
1559
|
+
this.onError?.(sessionID, loopID, err);
|
|
1560
|
+
throw err;
|
|
1561
|
+
}
|
|
1562
|
+
idleRace = armIdle();
|
|
1563
|
+
const msg = res.value;
|
|
1564
|
+
const eventResult = this.classifier.classify(msg, assistantContent);
|
|
1565
|
+
if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
|
|
1566
|
+
clearIdle();
|
|
1567
|
+
await this.persistFailed(sessionID, loopID, eventResult.err);
|
|
1568
|
+
this.broadcastError(sessionID, eventResult.err);
|
|
1569
|
+
this.onError?.(sessionID, loopID, eventResult.err);
|
|
1570
|
+
throw eventResult.err;
|
|
1571
|
+
}
|
|
1572
|
+
const step = (eventResult.thinkingStep ?? "").trim();
|
|
1573
|
+
if (step) this.broadcastThinkingStep(sessionID, step);
|
|
1574
|
+
if (eventResult.content) {
|
|
1575
|
+
if (eventResult.content.startsWith(assistantContent)) {
|
|
1576
|
+
assistantContent = eventResult.content;
|
|
1577
|
+
} else {
|
|
1578
|
+
assistantContent += eventResult.content;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const [final, deliverable] = this.classifier.resolveDeliverableFinalContent(
|
|
1582
|
+
eventResult,
|
|
1583
|
+
assistantContent
|
|
1584
|
+
);
|
|
1585
|
+
if (deliverable) {
|
|
1586
|
+
clearIdle();
|
|
1587
|
+
await this.completeTurn(
|
|
1588
|
+
sessionID,
|
|
1589
|
+
loopID,
|
|
1590
|
+
final,
|
|
1591
|
+
startedAt,
|
|
1592
|
+
eventResult.completionEvent ?? ""
|
|
1593
|
+
);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
} finally {
|
|
1598
|
+
clearIdle();
|
|
1599
|
+
clearTimeout(timer);
|
|
1600
|
+
this.gate.release(sessionID);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
async finishTimeout(sessionID, loopID, content, startedAt, failErr, completionEvent, policy) {
|
|
1604
|
+
if (policy === 1 /* SoftComplete */ && content.trim() !== "") {
|
|
1605
|
+
await this.completeTurn(sessionID, loopID, content, startedAt, completionEvent);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
await this.persistFailed(sessionID, loopID, failErr);
|
|
1609
|
+
this.broadcastError(sessionID, failErr);
|
|
1610
|
+
this.onError?.(sessionID, loopID, failErr);
|
|
1611
|
+
throw failErr;
|
|
1612
|
+
}
|
|
1613
|
+
async completeTurn(sessionID, loopID, final, startedAt, completionEvent) {
|
|
1614
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1615
|
+
await this.persistResponse(sessionID, loopID, final, startedAt, completionEvent);
|
|
1616
|
+
this.broadcastComplete(sessionID, final);
|
|
1617
|
+
this.onComplete?.(sessionID, loopID, final, completionEvent, elapsedMs);
|
|
1618
|
+
}
|
|
1619
|
+
async sendLoopCancel(_signal, conn, loopID) {
|
|
1620
|
+
const lid = (loopID ?? "").trim();
|
|
1621
|
+
if (!conn || !lid) return;
|
|
1622
|
+
const cancelMsg = { type: "command_request", command: "cancel", loop_id: lid };
|
|
1623
|
+
await conn.client.sendMessage(cancelMsg);
|
|
1624
|
+
}
|
|
1625
|
+
async persistResponse(sessionID, loopID, content, startedAt, completionEvent) {
|
|
1626
|
+
const msg = {
|
|
1627
|
+
role: "assistant",
|
|
1628
|
+
content,
|
|
1629
|
+
metadata: {
|
|
1630
|
+
started_at: startedAt,
|
|
1631
|
+
completed_at: Date.now(),
|
|
1632
|
+
duration_ms: Date.now() - startedAt,
|
|
1633
|
+
status: "completed",
|
|
1634
|
+
completion_event: completionEvent,
|
|
1635
|
+
deliverable: true
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
async persistFailed(sessionID, _loopID, err) {
|
|
1642
|
+
const msg = {
|
|
1643
|
+
role: "error",
|
|
1644
|
+
content: err.message,
|
|
1645
|
+
metadata: { status: "failed", error_message: err.message }
|
|
1646
|
+
};
|
|
1647
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
broadcastThinkingStep(sessionID, step) {
|
|
1651
|
+
if (!this.broadcaster) return;
|
|
1652
|
+
this.broadcaster.broadcast(sessionID, { type: "delta", data: step + "\n" });
|
|
1653
|
+
}
|
|
1654
|
+
broadcastComplete(sessionID, content) {
|
|
1655
|
+
this.broadcaster?.broadcast(sessionID, { type: "complete", data: content });
|
|
1656
|
+
}
|
|
1657
|
+
broadcastError(sessionID, err) {
|
|
1658
|
+
this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
// src/appkit/chunk_filter.ts
|
|
1663
|
+
var MSG_PAIR_LEN = 2;
|
|
1664
|
+
function updatesChunkIsNoop(data) {
|
|
1665
|
+
if (!data || typeof data !== "object") return true;
|
|
1666
|
+
return !("__interrupt__" in data);
|
|
1667
|
+
}
|
|
1668
|
+
function wireBody(msg) {
|
|
1669
|
+
for (const key of ["kwargs", "data"]) {
|
|
1670
|
+
const nested = msg[key];
|
|
1671
|
+
if (nested && typeof nested === "object") return nested;
|
|
1672
|
+
}
|
|
1673
|
+
return msg;
|
|
1674
|
+
}
|
|
1675
|
+
function dictHasToolInvocation(msg) {
|
|
1676
|
+
const body = wireBody(msg);
|
|
1677
|
+
if (body.tool_calls || body.tool_call_chunks) return true;
|
|
1678
|
+
for (const key of ["content", "content_blocks"]) {
|
|
1679
|
+
const raw = body[key];
|
|
1680
|
+
if (Array.isArray(raw)) {
|
|
1681
|
+
for (const item of raw) {
|
|
1682
|
+
if (item && typeof item === "object" && ["tool_call", "tool_call_chunk", "tool_use"].includes(
|
|
1683
|
+
String(item.type ?? "")
|
|
1684
|
+
)) {
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
function plainText(msg) {
|
|
1693
|
+
const body = wireBody(msg);
|
|
1694
|
+
const content = body.content ?? msg.content;
|
|
1695
|
+
if (typeof content === "string") return content;
|
|
1696
|
+
if (Array.isArray(content)) {
|
|
1697
|
+
const parts = [];
|
|
1698
|
+
for (const block of content) {
|
|
1699
|
+
if (typeof block === "string") parts.push(block);
|
|
1700
|
+
else if (block && typeof block === "object") {
|
|
1701
|
+
const text = block.text;
|
|
1702
|
+
if (typeof text === "string") parts.push(text);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return parts.join("");
|
|
1706
|
+
}
|
|
1707
|
+
return "";
|
|
1708
|
+
}
|
|
1709
|
+
function messageChunkIsNonActionable(data) {
|
|
1710
|
+
if (!Array.isArray(data) || data.length !== MSG_PAIR_LEN) return false;
|
|
1711
|
+
const msg = data[0];
|
|
1712
|
+
if (msg === null || msg === void 0) return true;
|
|
1713
|
+
if (!msg || typeof msg !== "object") return false;
|
|
1714
|
+
const m = msg;
|
|
1715
|
+
const body = wireBody(m);
|
|
1716
|
+
const raw = String(body.type ?? m.type ?? "");
|
|
1717
|
+
if (raw === "tool" || raw === "ToolMessage" || raw.endsWith("ToolMessage")) return false;
|
|
1718
|
+
if (dictHasToolInvocation(m)) return false;
|
|
1719
|
+
if (body.phase || m.phase) return false;
|
|
1720
|
+
return !plainText(m).trim();
|
|
1721
|
+
}
|
|
1722
|
+
function shouldDropStreamChunkEarly(_namespace, mode, data) {
|
|
1723
|
+
if (mode === "updates") return updatesChunkIsNoop(data);
|
|
1724
|
+
if (mode === "messages") return messageChunkIsNonActionable(data);
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/appkit/events.ts
|
|
1729
|
+
function unwrapNext(event) {
|
|
1730
|
+
if (!event || typeof event !== "object") return event;
|
|
1731
|
+
if (event.type !== "next") return event;
|
|
1732
|
+
const payload = event.payload;
|
|
1733
|
+
if (!payload || typeof payload !== "object") return event;
|
|
1734
|
+
const data = payload.data;
|
|
1735
|
+
return data && typeof data === "object" ? data : event;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/appkit/observability.ts
|
|
1739
|
+
var TurnEventStats = class {
|
|
1740
|
+
total = 0;
|
|
1741
|
+
messages = 0;
|
|
1742
|
+
updates = 0;
|
|
1743
|
+
custom = 0;
|
|
1744
|
+
skipped = 0;
|
|
1745
|
+
filteredEarly = 0;
|
|
1746
|
+
toolCalls = 0;
|
|
1747
|
+
toolResults = 0;
|
|
1748
|
+
textChunks = 0;
|
|
1749
|
+
heartbeatsDropped = 0;
|
|
1750
|
+
postIdleDrained = 0;
|
|
1751
|
+
inboundDropped = 0;
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
// src/appkit/daemon_session.ts
|
|
1755
|
+
var DEFAULT_POST_IDLE_DRAIN_MS = 500;
|
|
1756
|
+
var DaemonSession = class {
|
|
1757
|
+
wsUrl;
|
|
1758
|
+
workspace;
|
|
1759
|
+
streamDelivery;
|
|
1760
|
+
client;
|
|
1761
|
+
rpcClient;
|
|
1762
|
+
loopId = null;
|
|
1763
|
+
readBusy = false;
|
|
1764
|
+
rpcBusy = false;
|
|
1765
|
+
rpcConnected = false;
|
|
1766
|
+
streaming = false;
|
|
1767
|
+
postIdleDrainDeadlineMs;
|
|
1768
|
+
closed = false;
|
|
1769
|
+
earlyDropFn;
|
|
1770
|
+
statsFactory;
|
|
1771
|
+
config;
|
|
1772
|
+
turnEventStats;
|
|
1773
|
+
lastTurnEndState = null;
|
|
1774
|
+
lastTurnCancellationSeen = false;
|
|
1775
|
+
lastTurnErrorMessage = null;
|
|
1776
|
+
constructor(wsUrl, opts = {}) {
|
|
1777
|
+
this.wsUrl = wsUrl;
|
|
1778
|
+
this.workspace = opts.workspace;
|
|
1779
|
+
this.streamDelivery = opts.streamDelivery ?? "adaptive";
|
|
1780
|
+
this.config = opts.config ?? defaultConfig();
|
|
1781
|
+
this.client = new Client(wsUrl, this.config);
|
|
1782
|
+
this.rpcClient = new Client(wsUrl, this.config);
|
|
1783
|
+
this.postIdleDrainDeadlineMs = opts.postIdleDrainDeadlineMs && opts.postIdleDrainDeadlineMs > 0 ? opts.postIdleDrainDeadlineMs : DEFAULT_POST_IDLE_DRAIN_MS;
|
|
1784
|
+
this.earlyDropFn = opts.earlyDropFn ?? shouldDropStreamChunkEarly;
|
|
1785
|
+
this.statsFactory = opts.statsFactory ?? (() => new TurnEventStats());
|
|
1786
|
+
this.turnEventStats = this.statsFactory();
|
|
1787
|
+
}
|
|
1788
|
+
get streamClient() {
|
|
1789
|
+
return this.client;
|
|
1790
|
+
}
|
|
1791
|
+
get rpcSideClient() {
|
|
1792
|
+
return this.rpcClient;
|
|
1793
|
+
}
|
|
1794
|
+
get activeLoopId() {
|
|
1795
|
+
return this.loopId;
|
|
1796
|
+
}
|
|
1797
|
+
resolveStreamDeliveryMode() {
|
|
1798
|
+
const delivery = this.streamDelivery;
|
|
1799
|
+
if (typeof delivery === "function") return String(delivery() || "adaptive");
|
|
1800
|
+
return String(delivery || "adaptive");
|
|
1801
|
+
}
|
|
1802
|
+
get streamDeliveryMode() {
|
|
1803
|
+
return this.resolveStreamDeliveryMode();
|
|
1804
|
+
}
|
|
1805
|
+
shouldDrop(namespace, mode, data) {
|
|
1806
|
+
return Boolean(this.earlyDropFn(namespace, mode, data));
|
|
1807
|
+
}
|
|
1808
|
+
async connect(resumeLoopId) {
|
|
1809
|
+
await connectWithRetries(this.client);
|
|
1810
|
+
return this.bootstrapLoop(resumeLoopId ?? null);
|
|
1811
|
+
}
|
|
1812
|
+
async bootstrapLoop(resumeLoopId) {
|
|
1813
|
+
const loopNew = this.workspace ? { client_workspace: this.workspace, workspace: this.workspace } : void 0;
|
|
1814
|
+
const loopId = await bootstrapLoopSession(this.client, resumeLoopId, this.config, loopNew);
|
|
1815
|
+
this.loopId = loopId;
|
|
1816
|
+
return { type: "status", loop_id: loopId, state: "ready" };
|
|
1817
|
+
}
|
|
1818
|
+
async newLoop() {
|
|
1819
|
+
return this.bootstrapLoop(null);
|
|
1820
|
+
}
|
|
1821
|
+
async switchLoop(loopId) {
|
|
1822
|
+
return this.bootstrapLoop(loopId);
|
|
1823
|
+
}
|
|
1824
|
+
async ensureConnected() {
|
|
1825
|
+
if (this.client.isConnected() && !this.client.isDisconnected()) return;
|
|
1826
|
+
let resumeLoopId = this.loopId;
|
|
1827
|
+
if (this.rpcConnected) {
|
|
1828
|
+
this.rpcClient.close();
|
|
1829
|
+
this.rpcConnected = false;
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
await this.client.reconnect();
|
|
1833
|
+
} catch {
|
|
1834
|
+
this.client.close();
|
|
1835
|
+
await connectWithRetries(this.client);
|
|
1836
|
+
}
|
|
1837
|
+
if (resumeLoopId) {
|
|
1838
|
+
try {
|
|
1839
|
+
await this.client.reattachAndProbe(resumeLoopId);
|
|
1840
|
+
this.loopId = resumeLoopId;
|
|
1841
|
+
return;
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
if (!(err instanceof StaleLoopError)) throw err;
|
|
1844
|
+
resumeLoopId = null;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
await this.bootstrapLoop(resumeLoopId);
|
|
1848
|
+
}
|
|
1849
|
+
async close() {
|
|
1850
|
+
if (this.closed) return;
|
|
1851
|
+
this.closed = true;
|
|
1852
|
+
this.client.close();
|
|
1853
|
+
this.rpcClient.close();
|
|
1854
|
+
this.rpcConnected = false;
|
|
1855
|
+
}
|
|
1856
|
+
async detach() {
|
|
1857
|
+
if (!this.client.isConnected()) return;
|
|
1858
|
+
try {
|
|
1859
|
+
await this.client.notify("disconnect", {});
|
|
1860
|
+
} catch {
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async sendTurn(text, options) {
|
|
1864
|
+
if (!this.loopId) throw new Error("No active loop session");
|
|
1865
|
+
await this.client.sendInput(text, {
|
|
1866
|
+
loopID: this.loopId,
|
|
1867
|
+
autonomous: options?.autonomous,
|
|
1868
|
+
maxIterations: options?.maxIterations,
|
|
1869
|
+
subagent: options?.preferredSubagent,
|
|
1870
|
+
model: options?.model,
|
|
1871
|
+
modelParams: options?.modelParams,
|
|
1872
|
+
attachments: options?.attachments,
|
|
1873
|
+
clarificationMode: options?.clarificationMode,
|
|
1874
|
+
clarificationAnswer: options?.clarificationAnswer,
|
|
1875
|
+
intentHint: options?.intentHint
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
async cancelActiveTurn() {
|
|
1879
|
+
await this.client.notify("slash_command", { cmd: "/cancel" });
|
|
1880
|
+
}
|
|
1881
|
+
async *drainStreamEventsAfterIdle(expectedLoopId) {
|
|
1882
|
+
const deadline = Date.now() + this.postIdleDrainDeadlineMs;
|
|
1883
|
+
let exp = expectedLoopId;
|
|
1884
|
+
while (Date.now() < deadline) {
|
|
1885
|
+
const event = await this.client.readEventWithTimeout(250);
|
|
1886
|
+
if (!event) break;
|
|
1887
|
+
let frame = event;
|
|
1888
|
+
let eventType = String(frame.type ?? "");
|
|
1889
|
+
if (eventType === "next") {
|
|
1890
|
+
frame = unwrapNext(frame) ?? frame;
|
|
1891
|
+
eventType = String(frame.type ?? "");
|
|
1892
|
+
}
|
|
1893
|
+
const eventLoopId = frame.loop_id;
|
|
1894
|
+
if (exp && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== exp) {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (eventType === "error") {
|
|
1898
|
+
const errObj = frame.error ?? {};
|
|
1899
|
+
throw new Error(String(errObj.message || frame.message || "daemon error"));
|
|
1900
|
+
}
|
|
1901
|
+
if (eventType === "status") {
|
|
1902
|
+
const loopEv = frame.loop_id;
|
|
1903
|
+
if (typeof loopEv === "string" && loopEv) {
|
|
1904
|
+
this.loopId = loopEv;
|
|
1905
|
+
exp = loopEv;
|
|
1906
|
+
}
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (eventType !== "event") continue;
|
|
1910
|
+
const data = frame.data;
|
|
1911
|
+
const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
|
|
1912
|
+
const mode = String(frame.mode ?? "");
|
|
1913
|
+
if (this.shouldDrop(namespace, mode, data)) {
|
|
1914
|
+
this.turnEventStats.filteredEarly += 1;
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
this.turnEventStats.postIdleDrained += 1;
|
|
1918
|
+
yield [namespace, mode, data];
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async withRpcLock(fn) {
|
|
1922
|
+
while (this.rpcBusy) {
|
|
1923
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
1924
|
+
}
|
|
1925
|
+
this.rpcBusy = true;
|
|
1926
|
+
try {
|
|
1927
|
+
return await fn();
|
|
1928
|
+
} finally {
|
|
1929
|
+
this.rpcBusy = false;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
async ensureRpcConnected() {
|
|
1933
|
+
if (this.rpcConnected && this.rpcClient.isConnected()) return;
|
|
1934
|
+
await connectWithRetries(this.rpcClient);
|
|
1935
|
+
this.rpcConnected = true;
|
|
1936
|
+
}
|
|
1937
|
+
async listLoops(_limit = 20) {
|
|
1938
|
+
return this.withRpcLock(async () => {
|
|
1939
|
+
await this.ensureRpcConnected();
|
|
1940
|
+
return this.rpcClient.listLoops(15e3);
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
async fetchLoopCards(loopId) {
|
|
1944
|
+
const lid = String(loopId || "").trim();
|
|
1945
|
+
if (!lid) return { cards: [], seq: 0, contextTokens: 0, success: false };
|
|
1946
|
+
return this.withRpcLock(async () => {
|
|
1947
|
+
await this.ensureRpcConnected();
|
|
1948
|
+
try {
|
|
1949
|
+
const resp = await this.rpcClient.fetchLoopCards(lid, 3e4);
|
|
1950
|
+
const rawCards = resp.cards;
|
|
1951
|
+
return {
|
|
1952
|
+
cards: Array.isArray(rawCards) ? rawCards : [],
|
|
1953
|
+
seq: Number(resp.seq ?? 0),
|
|
1954
|
+
contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
|
|
1955
|
+
success: true
|
|
1956
|
+
};
|
|
1957
|
+
} catch {
|
|
1958
|
+
return { cards: [], seq: 0, contextTokens: 0, success: false };
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
async fetchLoopHistory(loopId) {
|
|
1963
|
+
const lid = String(loopId || "").trim();
|
|
1964
|
+
if (!lid) {
|
|
1965
|
+
return { goals: [], liveCards: [], liveGoalIndex: null, contextTokens: 0, success: false };
|
|
1966
|
+
}
|
|
1967
|
+
return this.withRpcLock(async () => {
|
|
1968
|
+
await this.ensureRpcConnected();
|
|
1969
|
+
try {
|
|
1970
|
+
const resp = await this.rpcClient.fetchLoopHistory(lid, 3e4);
|
|
1971
|
+
const liveGoalIndex = resp.live_goal_index;
|
|
1972
|
+
return {
|
|
1973
|
+
goals: Array.isArray(resp.goals) ? resp.goals : [],
|
|
1974
|
+
liveCards: Array.isArray(resp.live_cards) ? resp.live_cards : [],
|
|
1975
|
+
liveGoalIndex: typeof liveGoalIndex === "number" ? liveGoalIndex : null,
|
|
1976
|
+
contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
|
|
1977
|
+
success: Boolean(resp.success ?? true)
|
|
1978
|
+
};
|
|
1979
|
+
} catch {
|
|
1980
|
+
return {
|
|
1981
|
+
goals: [],
|
|
1982
|
+
liveCards: [],
|
|
1983
|
+
liveGoalIndex: null,
|
|
1984
|
+
contextTokens: 0,
|
|
1985
|
+
success: false
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
async fetchConversationLog(loopId, opts = {}) {
|
|
1991
|
+
const lid = String(loopId || "").trim();
|
|
1992
|
+
if (!lid) return [];
|
|
1993
|
+
return this.withRpcLock(async () => {
|
|
1994
|
+
await this.ensureRpcConnected();
|
|
1995
|
+
const resp = await this.rpcClient.getLoopMessages(
|
|
1996
|
+
lid,
|
|
1997
|
+
opts.limit ?? 100,
|
|
1998
|
+
opts.offset ?? 0,
|
|
1999
|
+
opts.includeEvents ?? false
|
|
2000
|
+
);
|
|
2001
|
+
const raw = resp.messages;
|
|
2002
|
+
if (!Array.isArray(raw)) return [];
|
|
2003
|
+
return raw.filter((m) => !!m && typeof m === "object");
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
async *iterTurnChunks(opts = {}) {
|
|
2007
|
+
this.turnEventStats = this.statsFactory();
|
|
2008
|
+
this.lastTurnEndState = null;
|
|
2009
|
+
this.lastTurnCancellationSeen = false;
|
|
2010
|
+
this.lastTurnErrorMessage = null;
|
|
2011
|
+
let queryStarted = false;
|
|
2012
|
+
let expectedLoopId = this.loopId;
|
|
2013
|
+
let streamPayloadSeen = false;
|
|
2014
|
+
let turnProgressSeen = false;
|
|
2015
|
+
this.streaming = true;
|
|
2016
|
+
const absoluteDeadline = opts.maxWaitMs !== void 0 && opts.maxWaitMs > 0 ? Date.now() + opts.maxWaitMs : null;
|
|
2017
|
+
this.client.peelStalePendingControlEvents();
|
|
2018
|
+
while (this.readBusy) {
|
|
2019
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
2020
|
+
}
|
|
2021
|
+
this.readBusy = true;
|
|
2022
|
+
try {
|
|
2023
|
+
while (true) {
|
|
2024
|
+
if (absoluteDeadline !== null && Date.now() >= absoluteDeadline) {
|
|
2025
|
+
throw new Error(
|
|
2026
|
+
`Turn timed out after ${opts.maxWaitMs}ms (loop=${expectedLoopId ?? "?"})`
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
const event = await this.client.readEvent();
|
|
2030
|
+
if (!event) {
|
|
2031
|
+
if (queryStarted && !this.client.isConnectionAlive()) {
|
|
2032
|
+
this.lastTurnEndState = "connection_lost";
|
|
2033
|
+
throw new Error("Daemon connection lost");
|
|
2034
|
+
}
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
let frame = event;
|
|
2038
|
+
let eventType = String(frame.type ?? "");
|
|
2039
|
+
if (eventType === "next") {
|
|
2040
|
+
frame = unwrapNext(frame) ?? frame;
|
|
2041
|
+
eventType = String(frame.type ?? "");
|
|
2042
|
+
}
|
|
2043
|
+
const eventLoopId = frame.loop_id;
|
|
2044
|
+
if (expectedLoopId && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== expectedLoopId) {
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
if (eventType === "error") {
|
|
2048
|
+
const errObj = frame.error ?? {};
|
|
2049
|
+
throw new Error(String(errObj.message || frame.message || "daemon error"));
|
|
2050
|
+
}
|
|
2051
|
+
if (eventType === "status") {
|
|
2052
|
+
const loopEv = frame.loop_id;
|
|
2053
|
+
if (typeof loopEv === "string" && loopEv) {
|
|
2054
|
+
this.loopId = loopEv;
|
|
2055
|
+
expectedLoopId = loopEv;
|
|
2056
|
+
}
|
|
2057
|
+
const state = String(frame.state ?? "");
|
|
2058
|
+
if (state === "running") {
|
|
2059
|
+
queryStarted = true;
|
|
2060
|
+
} else if (queryStarted && state === "stopped") {
|
|
2061
|
+
this.lastTurnEndState = state;
|
|
2062
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
2063
|
+
break;
|
|
2064
|
+
} else if (queryStarted && state === "idle") {
|
|
2065
|
+
if (!streamPayloadSeen && !this.lastTurnCancellationSeen) continue;
|
|
2066
|
+
this.lastTurnEndState = state;
|
|
2067
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
if (eventType === "command_response") {
|
|
2073
|
+
const content = String(frame.content ?? "");
|
|
2074
|
+
if (content.includes("Cancellation requested")) {
|
|
2075
|
+
this.lastTurnCancellationSeen = true;
|
|
2076
|
+
}
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (eventType !== "event") continue;
|
|
2080
|
+
const data = frame.data;
|
|
2081
|
+
const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
|
|
2082
|
+
const mode = String(frame.mode ?? "");
|
|
2083
|
+
if (this.shouldDrop(namespace, mode, data)) {
|
|
2084
|
+
this.turnEventStats.filteredEarly += 1;
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
if (mode === "custom" && isTurnEndCustomData(data)) {
|
|
2088
|
+
if (!queryStarted || !turnProgressSeen) continue;
|
|
2089
|
+
}
|
|
2090
|
+
streamPayloadSeen = true;
|
|
2091
|
+
if (isTurnProgressChunk(mode, data)) turnProgressSeen = true;
|
|
2092
|
+
yield [namespace, mode, data];
|
|
2093
|
+
if (mode === "custom" && isTurnEndCustomData(data)) {
|
|
2094
|
+
const customType = String(data.type ?? "").trim();
|
|
2095
|
+
this.lastTurnEndState = customType === STREAM_END ? "stream_end" : "completed";
|
|
2096
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
} catch (exc) {
|
|
2101
|
+
this.lastTurnErrorMessage = String(exc);
|
|
2102
|
+
throw exc;
|
|
2103
|
+
} finally {
|
|
2104
|
+
this.streaming = false;
|
|
2105
|
+
this.readBusy = false;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
326
2109
|
export {
|
|
2110
|
+
CLIENT_VERSION,
|
|
2111
|
+
ChatEventTerminal,
|
|
327
2112
|
Client,
|
|
2113
|
+
CommandClient,
|
|
328
2114
|
ConnectionError,
|
|
2115
|
+
ConnectionPool,
|
|
2116
|
+
DEFAULT_CLIENT_CAPABILITIES,
|
|
2117
|
+
DEFAULT_DELIVERABLE_PHASES,
|
|
2118
|
+
DEFAULT_POST_IDLE_DRAIN_MS,
|
|
2119
|
+
DEFAULT_THINKING_STEP_EVENTS,
|
|
329
2120
|
DaemonError,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
2121
|
+
DaemonSession,
|
|
2122
|
+
DisconnectCause,
|
|
2123
|
+
ErrIdleTimeout,
|
|
2124
|
+
ErrPoolExhausted,
|
|
2125
|
+
ErrQueryBusy,
|
|
2126
|
+
ErrQueryTimeout,
|
|
2127
|
+
EventAutopilotGoalCompleted,
|
|
2128
|
+
EventAutopilotGoalCreated,
|
|
2129
|
+
EventAutopilotGoalProgress,
|
|
2130
|
+
EventAutopilotGoalStatus,
|
|
2131
|
+
EventAutopilotWorkerAssigned,
|
|
2132
|
+
EventAutopilotWorkerUnassigned,
|
|
2133
|
+
EventCardCreated,
|
|
2134
|
+
EventCardReplayBegin,
|
|
2135
|
+
EventCardReplayEnd,
|
|
2136
|
+
EventClassifier,
|
|
335
2137
|
EventExploreCompleted,
|
|
336
2138
|
EventExploreMilestone,
|
|
337
2139
|
EventExploreStarted,
|
|
@@ -343,6 +2145,14 @@ export {
|
|
|
343
2145
|
EventMessageSent,
|
|
344
2146
|
EventPlanCreated,
|
|
345
2147
|
EventReplayComplete,
|
|
2148
|
+
EventStrangeLoopCompleted,
|
|
2149
|
+
EventStrangeLoopContextCompacted,
|
|
2150
|
+
EventStrangeLoopPlanDecision,
|
|
2151
|
+
EventStrangeLoopReasoned,
|
|
2152
|
+
EventStrangeLoopStarted,
|
|
2153
|
+
EventStrangeLoopStepCompleted,
|
|
2154
|
+
EventStrangeLoopStepQueued,
|
|
2155
|
+
EventStrangeLoopStepStarted,
|
|
346
2156
|
EventStreamToolCallUpdate,
|
|
347
2157
|
EventTacitusCompleted,
|
|
348
2158
|
EventTacitusGatherSummary,
|
|
@@ -351,31 +2161,76 @@ export {
|
|
|
351
2161
|
EventToolCompleted,
|
|
352
2162
|
EventToolError,
|
|
353
2163
|
EventToolStarted,
|
|
2164
|
+
INTENT_HINT_EMBED,
|
|
2165
|
+
INTENT_HINT_IMAGE_TO_TEXT,
|
|
2166
|
+
INTENT_HINT_OCR,
|
|
2167
|
+
INTENT_HINT_TEXT_COMPLETION,
|
|
2168
|
+
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
2169
|
+
PROTO_VERSION,
|
|
2170
|
+
PooledConn,
|
|
2171
|
+
QueryGate,
|
|
2172
|
+
REMOVED_INTENT_HINTS,
|
|
2173
|
+
ReconnectError,
|
|
2174
|
+
SSEBroadcaster,
|
|
2175
|
+
STREAM_END,
|
|
2176
|
+
StaleLoopError,
|
|
2177
|
+
StreamCloseFail,
|
|
2178
|
+
StreamCloseSoftComplete,
|
|
354
2179
|
TimeoutError,
|
|
2180
|
+
TimeoutPolicy,
|
|
2181
|
+
TurnEventStats,
|
|
2182
|
+
TurnRunner,
|
|
355
2183
|
VerbosityTier,
|
|
2184
|
+
authenticate,
|
|
356
2185
|
bootstrapLoopSession,
|
|
357
2186
|
checkDaemonStatus,
|
|
358
2187
|
classifyEventVerbosity,
|
|
2188
|
+
compactAttachments,
|
|
2189
|
+
compactImageAttachment,
|
|
359
2190
|
connectWithRetries,
|
|
2191
|
+
connectedWebsocket,
|
|
2192
|
+
connectionInitEnvelope,
|
|
360
2193
|
decodeMessage,
|
|
361
2194
|
defaultConfig,
|
|
2195
|
+
defaultPoolConfig,
|
|
2196
|
+
disconnectCauseName,
|
|
2197
|
+
disconnectEnvelope,
|
|
362
2198
|
encodeMessage,
|
|
363
2199
|
extractSootheLoopID,
|
|
2200
|
+
extractThinkingStep,
|
|
364
2201
|
fetchConfigSection,
|
|
2202
|
+
fetchLoopCards,
|
|
2203
|
+
fetchLoopHistory,
|
|
2204
|
+
fetchLoopMessages,
|
|
365
2205
|
fetchSkillsCatalog,
|
|
2206
|
+
idleTimeoutForTurn,
|
|
2207
|
+
inboundNeedsDeliveryAck,
|
|
2208
|
+
inputMessageForLoop,
|
|
366
2209
|
isCompletionEvent,
|
|
367
2210
|
isDaemonLive,
|
|
368
2211
|
isSubagentProgressEvent,
|
|
2212
|
+
isTurnEndCustomData,
|
|
2213
|
+
isTurnProgressChunk,
|
|
369
2214
|
isValidVerbosityLevel,
|
|
370
2215
|
loadConfigFromEnv,
|
|
371
2216
|
newLoopInputMessage,
|
|
372
2217
|
newLoopNewMessage,
|
|
373
2218
|
newLoopSubscribeMessage,
|
|
374
2219
|
newRequestID,
|
|
2220
|
+
notificationEnvelope,
|
|
375
2221
|
parseNamespace,
|
|
2222
|
+
pingEnvelope,
|
|
2223
|
+
pongEnvelope,
|
|
2224
|
+
protocol1Rpc,
|
|
2225
|
+
refreshAuthToken,
|
|
2226
|
+
requestDaemonConfigReload,
|
|
376
2227
|
requestDaemonShutdown,
|
|
2228
|
+
requestEnvelope,
|
|
377
2229
|
shouldShow,
|
|
378
2230
|
splitWirePayload,
|
|
2231
|
+
subscribeEnvelope,
|
|
2232
|
+
unsubscribeEnvelope,
|
|
2233
|
+
validateLoopInputIntentHint,
|
|
379
2234
|
waitDaemonReady,
|
|
380
2235
|
waitLoopStatusWithID,
|
|
381
2236
|
waitSubscriptionConfirmed
|