larkin 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-state-store.cjs +127 -21
- package/dist/agent/agent-state-store.mjs +128 -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/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 +66 -31
- package/dist/feishu/host-shell.mjs +66 -31
- package/dist/runtime/runtime-host.cjs +173 -10
- package/dist/runtime/runtime-host.mjs +173 -10
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { agentCliPromptCapabilities } from "../agent/agent-cli-capabilities.mjs";
|
|
5
|
+
import { isCanonicalInboxTarget, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
|
|
5
6
|
import { SpanKind } from "@opentelemetry/api";
|
|
6
7
|
import { classifyRuntimePrerequisite, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
7
8
|
import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
|
|
@@ -19,6 +20,20 @@ export class RuntimeSessionResetError extends Error {
|
|
|
19
20
|
const MAX_DELIVERIES = 2048;
|
|
20
21
|
const now = () => new Date().toISOString();
|
|
21
22
|
const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
|
|
23
|
+
const REPLAY_FAILURE_CODES = new Set([
|
|
24
|
+
"canonical_inbox_row_missing", "canonical_inbox_malformed", "duplicate_message_id",
|
|
25
|
+
"inbox_state_conflict", "delivery_target_conflict", "structured_target_invalid",
|
|
26
|
+
"structured_target_missing", "wake_reason_conflict",
|
|
27
|
+
]);
|
|
28
|
+
function replayFailureReason(code) {
|
|
29
|
+
return `Runtime delivery quarantined (${code}): no safe canonical Inbox target is available; the Inbox/ledger remain durable for operator recovery`;
|
|
30
|
+
}
|
|
31
|
+
function replayFailureCodeOf(record) {
|
|
32
|
+
if (record.status !== "error" || typeof record.reason !== "string")
|
|
33
|
+
return null;
|
|
34
|
+
const code = /^Runtime delivery quarantined \(([^)]+)\):/.exec(record.reason)?.[1];
|
|
35
|
+
return code && REPLAY_FAILURE_CODES.has(code) ? code : null;
|
|
36
|
+
}
|
|
22
37
|
function deliveryFile(agent) {
|
|
23
38
|
return agent.config.stateDir ? path.join(agent.config.stateDir, "runtime-deliveries.json") : null;
|
|
24
39
|
}
|
|
@@ -151,6 +166,81 @@ export function createRuntimeHost(options) {
|
|
|
151
166
|
emitConsumed(agent, persist(agent));
|
|
152
167
|
return agent.records.get(record.deliveryId) ?? record;
|
|
153
168
|
};
|
|
169
|
+
const scrubQuarantinedInput = (record, reason) => {
|
|
170
|
+
const staleInput = record.input && typeof record.input === "object" ? record.input : null;
|
|
171
|
+
record.input = {
|
|
172
|
+
inputId: typeof staleInput?.inputId === "string" && staleInput.inputId ? staleInput.inputId : record.deliveryId,
|
|
173
|
+
deliveryId: record.deliveryId,
|
|
174
|
+
kind: "wake",
|
|
175
|
+
text: reason,
|
|
176
|
+
attempt: Number.isSafeInteger(staleInput?.attempt) && Number(staleInput?.attempt) >= 0 ? Number(staleInput.attempt) : 0,
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
/** Rebuild every submitted notice from canonical structured state, never persisted text. */
|
|
180
|
+
const prepareRecordInput = (agent, record, busy) => {
|
|
181
|
+
if (record.target !== undefined && (typeof record.target !== "string" || !isCanonicalInboxTarget(record.target))) {
|
|
182
|
+
return { status: "error", code: "structured_target_invalid", reason: replayFailureReason("structured_target_invalid") };
|
|
183
|
+
}
|
|
184
|
+
if (record.wakeReason !== undefined && typeof record.wakeReason !== "string") {
|
|
185
|
+
return { status: "error", code: "wake_reason_conflict", reason: replayFailureReason("wake_reason_conflict") };
|
|
186
|
+
}
|
|
187
|
+
let target = typeof record.target === "string" ? record.target : null;
|
|
188
|
+
let wakeReason = typeof record.wakeReason === "string" ? record.wakeReason : undefined;
|
|
189
|
+
if (agent.stateStore?.resolveInboxDeliverySource) {
|
|
190
|
+
let source;
|
|
191
|
+
try {
|
|
192
|
+
source = agent.stateStore.resolveInboxDeliverySource(record.messageId);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
source = { status: "invalid", code: "canonical_inbox_malformed" };
|
|
196
|
+
}
|
|
197
|
+
if (source.status === "consumed")
|
|
198
|
+
return { status: "consumed" };
|
|
199
|
+
if (source.status === "missing" || source.status === "invalid") {
|
|
200
|
+
return { status: "error", code: source.code, reason: replayFailureReason(source.code) };
|
|
201
|
+
}
|
|
202
|
+
if (target && target !== source.target) {
|
|
203
|
+
return { status: "error", code: "delivery_target_conflict", reason: replayFailureReason("delivery_target_conflict") };
|
|
204
|
+
}
|
|
205
|
+
const canonicalWakeReason = typeof source.envelope.wake_reason === "string" ? source.envelope.wake_reason : undefined;
|
|
206
|
+
if (record.wakeReason !== undefined && wakeReason !== canonicalWakeReason) {
|
|
207
|
+
return { status: "error", code: "wake_reason_conflict", reason: replayFailureReason("wake_reason_conflict") };
|
|
208
|
+
}
|
|
209
|
+
target = source.target;
|
|
210
|
+
wakeReason = canonicalWakeReason;
|
|
211
|
+
}
|
|
212
|
+
if (!target)
|
|
213
|
+
return { status: "error", code: "structured_target_missing", reason: replayFailureReason("structured_target_missing") };
|
|
214
|
+
const staleInput = record.input && typeof record.input === "object" ? record.input : null;
|
|
215
|
+
const attempt = Number.isSafeInteger(staleInput?.attempt) && Number(staleInput?.attempt) >= 0 ? Number(staleInput.attempt) : 0;
|
|
216
|
+
const inputId = typeof staleInput?.inputId === "string" && staleInput.inputId ? staleInput.inputId : record.deliveryId;
|
|
217
|
+
record.target = target;
|
|
218
|
+
if (wakeReason)
|
|
219
|
+
record.wakeReason = wakeReason;
|
|
220
|
+
else
|
|
221
|
+
delete record.wakeReason;
|
|
222
|
+
record.input = {
|
|
223
|
+
inputId,
|
|
224
|
+
deliveryId: record.deliveryId,
|
|
225
|
+
kind: busy ? "inbox_update" : "wake",
|
|
226
|
+
text: options.promptBuilder.buildInboxNotice({ busy, count: 1, deliveryId: record.deliveryId, target,
|
|
227
|
+
...(wakeReason ? { wakeReason } : {}) }),
|
|
228
|
+
attempt,
|
|
229
|
+
};
|
|
230
|
+
return { status: "ready", target };
|
|
231
|
+
};
|
|
232
|
+
const quarantineRecord = (agent, record, code) => {
|
|
233
|
+
const reason = replayFailureReason(code);
|
|
234
|
+
scrubQuarantinedInput(record, reason);
|
|
235
|
+
record.reason = reason;
|
|
236
|
+
record.retryable = false;
|
|
237
|
+
const finalRecord = setRecord(agent, record, "error");
|
|
238
|
+
if (finalRecord.status === "consumed")
|
|
239
|
+
return { status: "accepted", deliveryId: record.deliveryId };
|
|
240
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
|
|
241
|
+
messageId: record.messageId, status: "error", reason });
|
|
242
|
+
return { status: "error", deliveryId: record.deliveryId, reason, retryable: false };
|
|
243
|
+
};
|
|
154
244
|
const reconcileExternalConsumption = (agent) => {
|
|
155
245
|
if (agent.stateStore) {
|
|
156
246
|
agent.stateStore.withInboxTransaction(() => reconcileFile(agent, agent.stateStore.readJson("runtimeDeliveries", { version: 1, records: [] })));
|
|
@@ -307,6 +397,20 @@ export function createRuntimeHost(options) {
|
|
|
307
397
|
if (!busy)
|
|
308
398
|
agent.busy = true; // Reserve the turn before prompt() can yield.
|
|
309
399
|
try {
|
|
400
|
+
const prepared = prepareRecordInput(agent, record, busy);
|
|
401
|
+
if (prepared.status === "consumed") {
|
|
402
|
+
if (!busy)
|
|
403
|
+
agent.busy = false;
|
|
404
|
+
const consumedRecord = setRecord(agent, record, "consumed");
|
|
405
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: consumedRecord.deliveryId,
|
|
406
|
+
messageId: consumedRecord.messageId, status: "consumed" });
|
|
407
|
+
return { status: "accepted", deliveryId: record.deliveryId };
|
|
408
|
+
}
|
|
409
|
+
if (prepared.status === "error") {
|
|
410
|
+
if (!busy)
|
|
411
|
+
agent.busy = false;
|
|
412
|
+
return quarantineRecord(agent, record, prepared.code);
|
|
413
|
+
}
|
|
310
414
|
const session = await ensureSession(agent);
|
|
311
415
|
const submittingRecord = setRecord(agent, record, "submitting");
|
|
312
416
|
if (submittingRecord.status === "consumed") {
|
|
@@ -799,7 +903,14 @@ export function createRuntimeHost(options) {
|
|
|
799
903
|
const deliveryFile = stateStore.readJson("runtimeDeliveries", { version: 1, records: [] });
|
|
800
904
|
if (!stateStore.readNdjson)
|
|
801
905
|
return deliveryFile;
|
|
802
|
-
|
|
906
|
+
let inboxRows;
|
|
907
|
+
try {
|
|
908
|
+
inboxRows = stateStore.readNdjson("inbox");
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
return deliveryFile;
|
|
912
|
+
}
|
|
913
|
+
const inboxIds = new Set(inboxRows.flatMap((row) => typeof row?.message_id === "string" ? [row.message_id] : []));
|
|
803
914
|
let changed = false;
|
|
804
915
|
const records = deliveryFile.records.map((record) => {
|
|
805
916
|
const synthetic = /^(?:redeliver_|rem_|interaction_)/.test(record.messageId);
|
|
@@ -822,11 +933,34 @@ export function createRuntimeHost(options) {
|
|
|
822
933
|
stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
|
|
823
934
|
configurationRecovery: null, stateStore, readiness: null,
|
|
824
935
|
turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
936
|
+
const startupConsumed = [];
|
|
937
|
+
const startupQuarantined = [];
|
|
938
|
+
for (const record of agent.records.values()) {
|
|
939
|
+
const existingQuarantine = replayFailureCodeOf(record);
|
|
940
|
+
if (existingQuarantine) {
|
|
941
|
+
startupQuarantined.push({ record, code: existingQuarantine });
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if (!isActiveDelivery(record.status))
|
|
945
|
+
continue;
|
|
946
|
+
record.status = "pending";
|
|
947
|
+
const prepared = prepareRecordInput(agent, record, false);
|
|
948
|
+
if (prepared.status === "consumed") {
|
|
949
|
+
record.status = "consumed";
|
|
950
|
+
record.updatedAt = now();
|
|
951
|
+
startupConsumed.push(record);
|
|
952
|
+
}
|
|
953
|
+
else if (prepared.status === "error") {
|
|
954
|
+
record.status = "error";
|
|
955
|
+
record.updatedAt = now();
|
|
956
|
+
scrubQuarantinedInput(record, prepared.reason);
|
|
957
|
+
record.reason = prepared.reason;
|
|
958
|
+
record.retryable = false;
|
|
959
|
+
startupQuarantined.push({ record, code: prepared.code });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
829
962
|
managed.set(config.agentId, agent);
|
|
963
|
+
emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
|
|
830
964
|
try {
|
|
831
965
|
await ensureSession(agent);
|
|
832
966
|
activeCount += 1;
|
|
@@ -844,6 +978,13 @@ export function createRuntimeHost(options) {
|
|
|
844
978
|
emit({ type: "agent-status", agentId: config.agentId, status: "error", error: reason,
|
|
845
979
|
...(error instanceof RuntimePrerequisiteError ? { readiness: error.readiness } : {}) });
|
|
846
980
|
}
|
|
981
|
+
const visibleQuarantines = startupQuarantined.filter(({ record }) => agent.records.get(record.deliveryId)?.status === "error");
|
|
982
|
+
for (const { record, code } of visibleQuarantines)
|
|
983
|
+
emit({ type: "delivery", agentId: config.agentId,
|
|
984
|
+
deliveryId: record.deliveryId, messageId: record.messageId, status: "error", reason: replayFailureReason(code) });
|
|
985
|
+
if (visibleQuarantines.length)
|
|
986
|
+
emit({ type: "agent-status", agentId: config.agentId, status: "error",
|
|
987
|
+
error: `${visibleQuarantines.length} Runtime delivery record(s) quarantined; canonical Inbox recovery is required` });
|
|
847
988
|
agent.poller = setInterval(() => reconcileExternalConsumption(agent), 250);
|
|
848
989
|
agent.poller.unref?.();
|
|
849
990
|
});
|
|
@@ -859,12 +1000,33 @@ export function createRuntimeHost(options) {
|
|
|
859
1000
|
telemetry?.delivery(agentId, messageId, "error");
|
|
860
1001
|
throw new Error(`unknown runtime Agent: ${agentId}`);
|
|
861
1002
|
}
|
|
1003
|
+
let target;
|
|
1004
|
+
try {
|
|
1005
|
+
target = targetKeyOfInboxEnvelope(envelope);
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
const reason = `Inbox target derivation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1009
|
+
const deliveryId = `invalid-target-${crypto.createHash("sha256").update(`${agentId}\0${messageId}`).digest("hex").slice(0, 24)}`;
|
|
1010
|
+
telemetry?.delivery(agentId, messageId, "error");
|
|
1011
|
+
emit({ type: "delivery", agentId, deliveryId, messageId, status: "error", reason });
|
|
1012
|
+
return { status: "error", deliveryId, reason, retryable: false };
|
|
1013
|
+
}
|
|
862
1014
|
reconcileExternalConsumption(agent);
|
|
863
1015
|
const existingId = agent.byMessage.get(messageId);
|
|
864
1016
|
if (existingId) {
|
|
865
1017
|
const existing = agent.records.get(existingId);
|
|
866
1018
|
if (existing?.status === "error") {
|
|
867
|
-
existing.
|
|
1019
|
+
if (existing.target && existing.target !== target)
|
|
1020
|
+
return quarantineRecord(agent, existing, "delivery_target_conflict");
|
|
1021
|
+
existing.target = target;
|
|
1022
|
+
if (typeof envelope.wake_reason === "string" && envelope.wake_reason)
|
|
1023
|
+
existing.wakeReason = envelope.wake_reason;
|
|
1024
|
+
else
|
|
1025
|
+
delete existing.wakeReason;
|
|
1026
|
+
const priorAttempt = existing.input && Number.isSafeInteger(existing.input.attempt) ? existing.input.attempt : 0;
|
|
1027
|
+
existing.input = existing.input && typeof existing.input === "object"
|
|
1028
|
+
? { ...existing.input, attempt: priorAttempt + 1 }
|
|
1029
|
+
: { inputId: existing.deliveryId, deliveryId: existing.deliveryId, kind: "wake", text: "", attempt: priorAttempt + 1 };
|
|
868
1030
|
delete existing.reason;
|
|
869
1031
|
delete existing.retryable;
|
|
870
1032
|
setRecord(agent, existing, "pending");
|
|
@@ -885,11 +1047,12 @@ export function createRuntimeHost(options) {
|
|
|
885
1047
|
}
|
|
886
1048
|
const deliveryId = crypto.randomUUID();
|
|
887
1049
|
const busy = agent.busy || agent.submitting;
|
|
1050
|
+
const wakeReason = typeof envelope.wake_reason === "string" ? envelope.wake_reason : undefined;
|
|
888
1051
|
const input = { inputId: deliveryId, deliveryId, kind: busy ? "inbox_update" : "wake",
|
|
889
|
-
text: options.promptBuilder.buildInboxNotice({ busy, count: 1, deliveryId,
|
|
890
|
-
...(
|
|
891
|
-
|
|
892
|
-
|
|
1052
|
+
text: options.promptBuilder.buildInboxNotice({ busy, count: 1, deliveryId, target,
|
|
1053
|
+
...(wakeReason ? { wakeReason } : {}) }), attempt: 0 };
|
|
1054
|
+
const record = { deliveryId, messageId, status: "pending", input, target,
|
|
1055
|
+
...(wakeReason ? { wakeReason } : {}), updatedAt: now() };
|
|
893
1056
|
agent.records.set(deliveryId, record);
|
|
894
1057
|
agent.byMessage.set(messageId, deliveryId);
|
|
895
1058
|
persist(agent);
|