larkin 0.3.3 → 0.4.2
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 +72 -33
- package/assets/readme/larkin-hero.svg +111 -0
- package/dist/agent/agent-state-store.cjs +321 -22
- package/dist/agent/agent-state-store.mjs +320 -22
- package/dist/agent/agent-transport.cjs +6 -4
- package/dist/agent/agent-transport.mjs +6 -4
- package/dist/agent/host-reminder-orchestrator.cjs +9 -3
- package/dist/agent/host-reminder-orchestrator.mjs +9 -3
- package/dist/agent/inbox-projection.cjs +114 -23
- package/dist/agent/inbox-projection.mjs +112 -23
- package/dist/app/cli.mjs +3 -1
- package/dist/app/local-control.mjs +108 -2
- package/dist/app/runtime-process.mjs +4 -0
- package/dist/app/session-cli.mjs +55 -20
- package/dist/feishu/document-comment.cjs +2 -1
- package/dist/feishu/document-comment.mjs +2 -1
- package/dist/feishu/host-business-state.cjs +7 -2
- package/dist/feishu/host-business-state.mjs +7 -2
- package/dist/feishu/host-shell.cjs +138 -32
- package/dist/feishu/host-shell.mjs +138 -32
- package/dist/runtime/pi-compaction-recovery.cjs +623 -0
- package/dist/runtime/pi-compaction-recovery.mjs +602 -0
- package/dist/runtime/pi-provider-config.cjs +2 -2
- package/dist/runtime/pi-provider-config.mjs +2 -2
- package/dist/runtime/pi-rpc-client.cjs +39 -22
- package/dist/runtime/pi-rpc-client.mjs +39 -22
- package/dist/runtime/provider-error-classifier.cjs +39 -0
- package/dist/runtime/provider-error-classifier.mjs +34 -0
- package/dist/runtime/runtime-adapters.mjs +82 -19
- package/dist/runtime/runtime-host.cjs +663 -12
- package/dist/runtime/runtime-host.mjs +661 -11
- package/package.json +1 -1
|
@@ -160,6 +160,16 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
160
160
|
return store;
|
|
161
161
|
};
|
|
162
162
|
const hostState = new HostStateProjection(stateStore, log);
|
|
163
|
+
const recordInboundDeliveryFailure = (agent, code) => {
|
|
164
|
+
const at = new Date().toISOString();
|
|
165
|
+
const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
|
|
166
|
+
const nextAction = "Inspect the delivery/status error, correct the Runtime or canonical Inbox state, then restart to replay safely.";
|
|
167
|
+
hostState.updateStatus(agent, {
|
|
168
|
+
inboundDeliveryHealth: { state: "error", code, at, reason, nextAction },
|
|
169
|
+
runtimeReadiness: { runtime: agent.runtime, state: "incompatible", reason, nextAction },
|
|
170
|
+
});
|
|
171
|
+
hostState.recordStatusError(agent, `${reason} ${nextAction} code=${code}`);
|
|
172
|
+
};
|
|
163
173
|
const agentStates = new Map();
|
|
164
174
|
const saveAgentState = (record) => {
|
|
165
175
|
try {
|
|
@@ -295,6 +305,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
295
305
|
return;
|
|
296
306
|
}
|
|
297
307
|
const telemetryMessageId = String(event.message_id || event.event_id || eventKey);
|
|
308
|
+
let canonicalInboxDurable = false;
|
|
298
309
|
if (wake)
|
|
299
310
|
telemetry?.beginMessage(agent.agentId, telemetryMessageId);
|
|
300
311
|
if (event.event_id)
|
|
@@ -305,58 +316,76 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
305
316
|
senderIdentity.ensureChatNames(agent, event.chat_id, 3_000),
|
|
306
317
|
event._sender_is_bot ? Promise.resolve(null) : senderIdentity.ensureSenderSignature(agent, event.sender_id, 3_000),
|
|
307
318
|
]);
|
|
308
|
-
const
|
|
309
|
-
|
|
319
|
+
const projected = envelopeProjector.projectInbound(agent, event, { anchorReply: wake, names, signature });
|
|
320
|
+
projected.target = targetKeyOfInboxEnvelope({ ...projected, chat_id: event.chat_id, thread_id: event.thread_id });
|
|
310
321
|
if (wake)
|
|
311
|
-
|
|
312
|
-
const
|
|
322
|
+
projected.wake = true;
|
|
323
|
+
const candidate = projectInboxEnvelope(projected, {
|
|
313
324
|
chat_id: event.chat_id,
|
|
314
325
|
thread_id: event.thread_id,
|
|
315
326
|
...(event.create_time !== undefined ? { create_time: String(event.create_time) } : {}),
|
|
316
327
|
sender_id: event.sender_id,
|
|
317
|
-
content: String(
|
|
328
|
+
content: String(projected.content ?? event.content ?? ""),
|
|
318
329
|
});
|
|
330
|
+
let append;
|
|
319
331
|
try {
|
|
320
|
-
stateStore(agent).
|
|
332
|
+
append = stateStore(agent).appendCanonicalInboxOnce(candidate);
|
|
321
333
|
}
|
|
322
334
|
catch (error) {
|
|
323
335
|
throw new Error(`inbox 写失败: ${errorMessage(error)}`);
|
|
324
336
|
}
|
|
325
|
-
|
|
326
|
-
//
|
|
337
|
+
canonicalInboxDurable = true;
|
|
338
|
+
// An event becomes permanently transport-seen only after the canonical
|
|
339
|
+
// append/dedupe decision is durable. Agent model-seen state is untouched.
|
|
327
340
|
if (event.event_id)
|
|
328
341
|
seenEventIds.add(eventKey);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
342
|
+
if (append.status === "duplicate_consumed")
|
|
343
|
+
return null;
|
|
344
|
+
const inboxEnvelope = append.envelope;
|
|
345
|
+
if (append.status === "appended")
|
|
346
|
+
hostState.appendConversation(agent, {
|
|
347
|
+
direction: "in", from: inboxEnvelope.sender_name, senderType: inboxEnvelope.sender_type,
|
|
348
|
+
target: String(inboxEnvelope.target || targetFor(event).target), wake, text: event.content, messageId: inboxEnvelope.message_id,
|
|
349
|
+
at: inboxEnvelope.timestamp || new Date().toISOString(),
|
|
350
|
+
});
|
|
351
|
+
return inboxEnvelope;
|
|
335
352
|
};
|
|
336
|
-
const
|
|
353
|
+
const inboxEnvelope = wake && telemetry
|
|
337
354
|
? await telemetry.phase(telemetryMessageId, "feishu.receive", SpanKind.CONSUMER, receive)
|
|
338
355
|
: await receive();
|
|
339
|
-
if (!wake)
|
|
356
|
+
if (!wake || !inboxEnvelope)
|
|
340
357
|
return;
|
|
341
|
-
const receipt = await runtimeHost.deliver(agent.agentId,
|
|
342
|
-
if (receipt.status === "
|
|
358
|
+
const receipt = await runtimeHost.deliver(agent.agentId, inboxEnvelope);
|
|
359
|
+
if (receipt.status === "error") {
|
|
360
|
+
recordInboundDeliveryFailure(agent, "non_retryable_receipt");
|
|
343
361
|
hostState.appendStatusLog(agent, "deliverLog", {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}, 30);
|
|
349
|
-
if (envelope.sender_type === "human" || envelope.sender_type === "agent")
|
|
350
|
-
processingEyes.add(agent, String(envelope.message_id || ""));
|
|
351
|
-
if (receipt.status === "deferred")
|
|
352
|
-
log(`Runtime 暂缓投递,消息保留在 inbox seq=${envelope.seq}: ${receipt.reason}`);
|
|
362
|
+
at: new Date().toISOString(), status: "error",
|
|
363
|
+
reason: "non-retryable Runtime delivery error; durable recovery state retained",
|
|
364
|
+
}, 80);
|
|
365
|
+
return;
|
|
353
366
|
}
|
|
367
|
+
hostState.appendStatusLog(agent, "deliverLog", {
|
|
368
|
+
from: inboxEnvelope.sender_name,
|
|
369
|
+
target: String(inboxEnvelope.target || targetFor(event).target),
|
|
370
|
+
excerpt: safeConversationExcerpt(event.content, 180),
|
|
371
|
+
at: new Date().toISOString(),
|
|
372
|
+
}, 30);
|
|
373
|
+
if (inboxEnvelope.sender_type === "human" || inboxEnvelope.sender_type === "agent")
|
|
374
|
+
processingEyes.add(agent, String(inboxEnvelope.message_id || ""));
|
|
375
|
+
if (receipt.status === "deferred")
|
|
376
|
+
log(`Runtime 暂缓投递,消息保留在 inbox seq=${inboxEnvelope.seq}: ${receipt.reason}`);
|
|
354
377
|
}
|
|
355
378
|
catch (error) {
|
|
356
379
|
if (wake)
|
|
357
380
|
telemetry?.delivery(agent.agentId, telemetryMessageId, "error");
|
|
358
|
-
|
|
359
|
-
|
|
381
|
+
if (canonicalInboxDurable) {
|
|
382
|
+
log(`onFeishuMessage Runtime delivery failed agent=${agent.name}; canonical Inbox retained`);
|
|
383
|
+
recordInboundDeliveryFailure(agent, "runtime_delivery_exception");
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
log(`onFeishuMessage 异常 agent=${agent.name}: ${error instanceof Error ? error.stack || error.message : String(error)}`);
|
|
387
|
+
hostState.recordStatusError(agent, `onFeishuMessage: ${errorMessage(error)}`);
|
|
388
|
+
}
|
|
360
389
|
}
|
|
361
390
|
finally {
|
|
362
391
|
if (event.event_id)
|
|
@@ -1201,16 +1230,24 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
1201
1230
|
return;
|
|
1202
1231
|
}
|
|
1203
1232
|
if (message.type === "delivery") {
|
|
1233
|
+
if (message.status === "error")
|
|
1234
|
+
recordInboundDeliveryFailure(agent, "runtime_delivery_event");
|
|
1204
1235
|
hostState.appendStatusLog(agent, "deliverLog", {
|
|
1205
1236
|
at: new Date().toISOString(), deliveryId: message.deliveryId, messageId: message.messageId,
|
|
1206
|
-
status: message.status,
|
|
1237
|
+
status: message.status,
|
|
1238
|
+
...(message.status === "error"
|
|
1239
|
+
? { reason: "non-retryable Runtime delivery error; durable recovery state retained" }
|
|
1240
|
+
: (message.reason ? { reason: safeConversationExcerpt(message.reason, 120) } : {})),
|
|
1207
1241
|
}, 80);
|
|
1208
1242
|
return;
|
|
1209
1243
|
}
|
|
1210
1244
|
if (message.type === "session") {
|
|
1211
1245
|
const record = agentStates.get(agent.agentId);
|
|
1212
1246
|
if (record && record.state.sessions[message.runtime] !== message.sessionId) {
|
|
1213
|
-
|
|
1247
|
+
if (message.sessionId)
|
|
1248
|
+
record.state.sessions[message.runtime] = message.sessionId;
|
|
1249
|
+
else
|
|
1250
|
+
delete record.state.sessions[message.runtime];
|
|
1214
1251
|
saveAgentState(record);
|
|
1215
1252
|
log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
|
|
1216
1253
|
}
|
|
@@ -1226,7 +1263,8 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
1226
1263
|
hostState.recordStatusError(agent, `auth: ${readiness.reason}; ${readiness.nextAction}`);
|
|
1227
1264
|
}
|
|
1228
1265
|
else {
|
|
1229
|
-
|
|
1266
|
+
const category = message.event.errorCategory || "provider";
|
|
1267
|
+
hostState.recordStatusError(agent, `${category}: Runtime input failed ${message.event.retryable ? "retryably" : "non-retryably"}; inspect delivery health and Runtime/provider configuration`);
|
|
1230
1268
|
}
|
|
1231
1269
|
}
|
|
1232
1270
|
};
|
|
@@ -1357,6 +1395,74 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
1357
1395
|
: unavailable ? "reset committed but Runtime/channel readiness is unavailable"
|
|
1358
1396
|
: "reset committed but readiness did not converge before timeout" };
|
|
1359
1397
|
},
|
|
1398
|
+
async recoverSession(agentId, reason, waitReadyMs = 30_000) {
|
|
1399
|
+
const agent = agents.find((candidate) => candidate.agentId === agentId);
|
|
1400
|
+
if (!agent)
|
|
1401
|
+
throw Object.assign(new Error(`未知 Agent: ${agentId}`), { code: "unknown_agent" });
|
|
1402
|
+
if (reason !== "context-overflow")
|
|
1403
|
+
throw Object.assign(new Error("unsupported recovery reason"), { code: "recovery_refused" });
|
|
1404
|
+
if (!runtimeHost.recoverSession)
|
|
1405
|
+
throw Object.assign(new Error("Runtime session recovery 尚未就绪"), { code: "recovery_unavailable" });
|
|
1406
|
+
const connectionState = (status = hostState.readStatus(agent)) => {
|
|
1407
|
+
const connectedAt = Date.parse(String(status.connectedAt || ""));
|
|
1408
|
+
const channelConnected = Number.isFinite(connectedAt) && connectedAt >= Date.parse(daemonStartedAt) - 1000;
|
|
1409
|
+
return { channelConnected, reconnecting: isChannelReconnecting(status) };
|
|
1410
|
+
};
|
|
1411
|
+
const readinessProjection = () => {
|
|
1412
|
+
const status = hostState.readStatus(agent);
|
|
1413
|
+
const turns = status.session && typeof status.session === "object"
|
|
1414
|
+
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1415
|
+
const runtimeReady = Boolean(status.runtimeReadiness && typeof status.runtimeReadiness === "object"
|
|
1416
|
+
&& status.runtimeReadiness.state === "ready");
|
|
1417
|
+
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1418
|
+
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
1419
|
+
};
|
|
1420
|
+
const initialProjection = readinessProjection();
|
|
1421
|
+
if (!initialProjection.channelConnected)
|
|
1422
|
+
throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
|
|
1423
|
+
if (initialProjection.reconnecting)
|
|
1424
|
+
throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
|
|
1425
|
+
let recovery;
|
|
1426
|
+
try {
|
|
1427
|
+
recovery = await runtimeHost.recoverSession(agentId, reason);
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
|
|
1431
|
+
}
|
|
1432
|
+
const record = agentStates.get(agentId);
|
|
1433
|
+
try {
|
|
1434
|
+
if (record) {
|
|
1435
|
+
if (recovery.sessionId)
|
|
1436
|
+
record.state.sessions[agent.runtime] = recovery.sessionId;
|
|
1437
|
+
else
|
|
1438
|
+
delete record.state.sessions[agent.runtime];
|
|
1439
|
+
record.store.writeJson("agentState", record.state);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
catch (error) {
|
|
1443
|
+
const projection = readinessProjection();
|
|
1444
|
+
return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
|
|
1445
|
+
...projection, rearmedCount: recovery.rearmedCount, replayStatus: "pending", remainingPendingCount: projection.pendingCount,
|
|
1446
|
+
readyForFreshScenario: false, inboundObserved: false, code: "state_persistence_failed",
|
|
1447
|
+
error: "recovery committed but Agent session state persistence failed" };
|
|
1448
|
+
}
|
|
1449
|
+
const deadline = Date.now() + Math.max(0, waitReadyMs);
|
|
1450
|
+
let projection = readinessProjection();
|
|
1451
|
+
do {
|
|
1452
|
+
projection = readinessProjection();
|
|
1453
|
+
if (projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting)
|
|
1454
|
+
break;
|
|
1455
|
+
if (Date.now() >= deadline)
|
|
1456
|
+
break;
|
|
1457
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
|
|
1458
|
+
} while (true);
|
|
1459
|
+
const replayStatus = projection.pendingCount === 0 ? "consumed" : "pending";
|
|
1460
|
+
const ready = projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting;
|
|
1461
|
+
return { recoveryCommitted: true, generationChanged: recovery.generationChanged, sessionChanged: recovery.sessionChanged,
|
|
1462
|
+
...projection, rearmedCount: recovery.rearmedCount, replayStatus, remainingPendingCount: projection.pendingCount,
|
|
1463
|
+
readyForFreshScenario: ready, inboundObserved: false,
|
|
1464
|
+
...(!ready ? { code: "recovery_timeout", error: "recovery committed but Runtime/channel readiness did not converge before timeout" } : {}) };
|
|
1465
|
+
},
|
|
1360
1466
|
async start() {
|
|
1361
1467
|
try {
|
|
1362
1468
|
fs.mkdirSync(larkinHome, { recursive: true });
|