larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RuntimeSessionResetError = void 0;
|
|
7
|
+
exports.createRuntimeHost = createRuntimeHost;
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const agent_cli_capabilities_js_1 = require("../agent/agent-cli-capabilities.cjs");
|
|
12
|
+
const api_1 = require("@opentelemetry/api");
|
|
13
|
+
const runtime_readiness_js_1 = require("./runtime-readiness.cjs");
|
|
14
|
+
const official_lark_cli_js_1 = require("../app/official-lark-cli.cjs");
|
|
15
|
+
const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
|
|
16
|
+
class RuntimeSessionResetError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
pendingCount;
|
|
19
|
+
constructor(code, message, pendingCount = 0) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.pendingCount = pendingCount;
|
|
23
|
+
this.name = "RuntimeSessionResetError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.RuntimeSessionResetError = RuntimeSessionResetError;
|
|
27
|
+
const MAX_DELIVERIES = 2048;
|
|
28
|
+
const now = () => new Date().toISOString();
|
|
29
|
+
const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
|
|
30
|
+
function deliveryFile(agent) {
|
|
31
|
+
return agent.config.stateDir ? node_path_1.default.join(agent.config.stateDir, "runtime-deliveries.json") : null;
|
|
32
|
+
}
|
|
33
|
+
function readDeliveryFile(file) {
|
|
34
|
+
if (!file)
|
|
35
|
+
return { version: 1, records: [] };
|
|
36
|
+
try {
|
|
37
|
+
const stat = node_fs_1.default.lstatSync(file);
|
|
38
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
39
|
+
throw new Error("delivery state is not a regular file");
|
|
40
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
41
|
+
return { version: 1, records: Array.isArray(parsed.records) ? parsed.records.filter((record) => Boolean(record?.deliveryId && record?.messageId && ["pending", "submitting", "accepted", "consumed", "error"].includes(record.status))) : [] };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ENOENT")
|
|
45
|
+
return { version: 1, records: [] };
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function persist(agent) {
|
|
50
|
+
const externallyConsumed = [];
|
|
51
|
+
if (agent.stateStore) {
|
|
52
|
+
agent.stateStore.withInboxTransaction(() => {
|
|
53
|
+
const external = agent.stateStore.readJson("runtimeDeliveries", { version: 1, records: [] });
|
|
54
|
+
for (const record of external.records || []) {
|
|
55
|
+
const current = agent.records.get(record.deliveryId);
|
|
56
|
+
if (current && current.status !== "consumed" && record.status === "consumed") {
|
|
57
|
+
agent.records.set(record.deliveryId, record);
|
|
58
|
+
externallyConsumed.push(record);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const records = boundedRecords(agent);
|
|
62
|
+
agent.stateStore.writeJson("runtimeDeliveries", { version: 1, records });
|
|
63
|
+
});
|
|
64
|
+
return externallyConsumed;
|
|
65
|
+
}
|
|
66
|
+
const file = deliveryFile(agent);
|
|
67
|
+
if (!file)
|
|
68
|
+
return externallyConsumed;
|
|
69
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true, mode: 0o700 });
|
|
70
|
+
const existing = (() => { try {
|
|
71
|
+
return node_fs_1.default.lstatSync(file);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
} })();
|
|
76
|
+
if (existing?.isSymbolicLink())
|
|
77
|
+
throw new Error("delivery state must not be a symlink");
|
|
78
|
+
const bounded = boundedRecords(agent);
|
|
79
|
+
const temporary = node_path_1.default.join(node_path_1.default.dirname(file), `.${node_path_1.default.basename(file)}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
|
|
80
|
+
node_fs_1.default.writeFileSync(temporary, `${JSON.stringify({ version: 1, records: bounded }, null, 2)}\n`, { mode: 0o600 });
|
|
81
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
82
|
+
node_fs_1.default.chmodSync(file, 0o600);
|
|
83
|
+
return externallyConsumed;
|
|
84
|
+
}
|
|
85
|
+
function boundedRecords(agent) {
|
|
86
|
+
const records = [...agent.records.values()]
|
|
87
|
+
.sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
|
|
88
|
+
const active = records.filter((record) => isActiveDelivery(record.status));
|
|
89
|
+
if (active.length > MAX_DELIVERIES)
|
|
90
|
+
throw new Error(`runtime delivery backlog exceeds ${MAX_DELIVERIES}`);
|
|
91
|
+
const terminal = records.filter((record) => !isActiveDelivery(record.status)).slice(-(MAX_DELIVERIES - Math.min(active.length, MAX_DELIVERIES)));
|
|
92
|
+
const bounded = [...active, ...terminal].slice(-MAX_DELIVERIES);
|
|
93
|
+
agent.records = new Map(bounded.map((record) => [record.deliveryId, record]));
|
|
94
|
+
agent.byMessage = new Map(bounded.map((record) => [record.messageId, record.deliveryId]));
|
|
95
|
+
return bounded;
|
|
96
|
+
}
|
|
97
|
+
function createRuntimeHost(options) {
|
|
98
|
+
const managed = new Map();
|
|
99
|
+
const listeners = new Set();
|
|
100
|
+
const log = options.log ?? (() => { });
|
|
101
|
+
const telemetry = options.telemetry;
|
|
102
|
+
const retryPolicy = {
|
|
103
|
+
baseDelayMs: options.retryPolicy?.baseDelayMs ?? 250,
|
|
104
|
+
maxDelayMs: options.retryPolicy?.maxDelayMs ?? 10_000,
|
|
105
|
+
maxAttempts: options.retryPolicy?.maxAttempts ?? 6,
|
|
106
|
+
stableWindowMs: options.retryPolicy?.stableWindowMs ?? 30_000,
|
|
107
|
+
};
|
|
108
|
+
const emit = (event) => {
|
|
109
|
+
if (event.type === "delivery")
|
|
110
|
+
telemetry?.delivery(event.agentId, event.messageId, event.status);
|
|
111
|
+
for (const listener of listeners)
|
|
112
|
+
listener(event);
|
|
113
|
+
};
|
|
114
|
+
const runtimeEnv = (config, generation) => {
|
|
115
|
+
const base = {
|
|
116
|
+
LARKIN_AGENT_ID: config.agentId,
|
|
117
|
+
...(generation ? { LARKIN_RUNTIME_OBSERVATION_GENERATION: generation } : {}),
|
|
118
|
+
LARKIN_CONFIG_DIR: process.env.LARKIN_CONFIG_DIR,
|
|
119
|
+
LARKIN_HOME: process.env.LARKIN_HOME,
|
|
120
|
+
...(config.piDistribution ? { LARKIN_PI_DISTRIBUTION: config.piDistribution } : {}),
|
|
121
|
+
...(process.env.HOME ? { HOME: process.env.HOME } : {}),
|
|
122
|
+
...(process.env.SHELL ? { SHELL: process.env.SHELL } : {}),
|
|
123
|
+
...(process.env.ZDOTDIR ? { ZDOTDIR: process.env.ZDOTDIR } : {}),
|
|
124
|
+
...(process.env.BASH_ENV ? { BASH_ENV: process.env.BASH_ENV } : {}),
|
|
125
|
+
PATH: [config.stateDir ? node_path_1.default.join(config.stateDir, "runtime-bin") : null, process.env.PATH].filter(Boolean).join(node_path_1.default.delimiter),
|
|
126
|
+
LARKSUITE_CLI_CONFIG_DIR: config.larkConfigDir ?? process.env.LARKSUITE_CLI_CONFIG_DIR,
|
|
127
|
+
};
|
|
128
|
+
return config.larkConfigDir ? (0, agent_lark_cli_workspace_js_1.managedLarkCliEnv)(config, base) : base;
|
|
129
|
+
};
|
|
130
|
+
const assertOfficialCliReady = async (config, env) => {
|
|
131
|
+
if (!config.larkConfigDir)
|
|
132
|
+
return;
|
|
133
|
+
if (options.assertOfficialCliReady)
|
|
134
|
+
await options.assertOfficialCliReady(config, env);
|
|
135
|
+
else {
|
|
136
|
+
(0, official_lark_cli_js_1.resolveOfficialLarkCli)({ env });
|
|
137
|
+
(0, agent_lark_cli_workspace_js_1.assertAgentWorkspaceBound)(config);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const emitConsumed = (agent, records) => {
|
|
141
|
+
for (const record of records)
|
|
142
|
+
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
143
|
+
deliveryId: record.deliveryId, messageId: record.messageId, status: "consumed" });
|
|
144
|
+
};
|
|
145
|
+
const markSessionStable = (agent, session) => {
|
|
146
|
+
if (agent.session !== session || agent.stopped)
|
|
147
|
+
return;
|
|
148
|
+
if (agent.stabilityTimer)
|
|
149
|
+
clearTimeout(agent.stabilityTimer);
|
|
150
|
+
agent.stabilityTimer = null;
|
|
151
|
+
agent.recreateAttempts = 0;
|
|
152
|
+
agent.recreateReason = null;
|
|
153
|
+
};
|
|
154
|
+
const setRecord = (agent, record, status) => {
|
|
155
|
+
record.status = status;
|
|
156
|
+
record.updatedAt = now();
|
|
157
|
+
agent.records.set(record.deliveryId, record);
|
|
158
|
+
agent.byMessage.set(record.messageId, record.deliveryId);
|
|
159
|
+
emitConsumed(agent, persist(agent));
|
|
160
|
+
return agent.records.get(record.deliveryId) ?? record;
|
|
161
|
+
};
|
|
162
|
+
const reconcileExternalConsumption = (agent) => {
|
|
163
|
+
if (agent.stateStore) {
|
|
164
|
+
agent.stateStore.withInboxTransaction(() => reconcileFile(agent, agent.stateStore.readJson("runtimeDeliveries", { version: 1, records: [] })));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const file = deliveryFile(agent);
|
|
168
|
+
if (!file)
|
|
169
|
+
return;
|
|
170
|
+
let external;
|
|
171
|
+
try {
|
|
172
|
+
external = readDeliveryFile(file);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
log("delivery reconcile failed", String(error));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
reconcileFile(agent, external);
|
|
179
|
+
};
|
|
180
|
+
const reconcileFile = (agent, external) => {
|
|
181
|
+
for (const next of external.records || []) {
|
|
182
|
+
const current = agent.records.get(next.deliveryId);
|
|
183
|
+
if (current && current.status !== "consumed" && next.status === "consumed") {
|
|
184
|
+
agent.records.set(next.deliveryId, next);
|
|
185
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: next.deliveryId, messageId: next.messageId, status: "consumed" });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const TURN_END_RETRY_REASON = "runtime turn ended before Inbox consumption was observed";
|
|
190
|
+
/**
|
|
191
|
+
* Runtime acceptance acknowledges only the notification. At the safe turn
|
|
192
|
+
* boundary, canonical Inbox presence decides whether that same delivery
|
|
193
|
+
* identity must become retryable again. The Inbox rows and ledger update are
|
|
194
|
+
* observed under one transaction so a concurrent poll cannot be overwritten.
|
|
195
|
+
*/
|
|
196
|
+
const reconcileAcceptedAtTurnEnd = (agent) => {
|
|
197
|
+
const store = agent.stateStore;
|
|
198
|
+
if (!store?.readNdjson) {
|
|
199
|
+
reconcileExternalConsumption(agent);
|
|
200
|
+
for (const record of agent.records.values()) {
|
|
201
|
+
if (record.status !== "accepted")
|
|
202
|
+
continue;
|
|
203
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
|
|
204
|
+
messageId: record.messageId, status: "deferred", reason: TURN_END_RETRY_REASON });
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const consumed = [];
|
|
209
|
+
const deferred = [];
|
|
210
|
+
try {
|
|
211
|
+
store.withInboxTransaction(() => {
|
|
212
|
+
const inboxIds = new Set(store.readNdjson("inbox").flatMap((row) => typeof row?.message_id === "string" ? [row.message_id] : []));
|
|
213
|
+
const disk = store.readJson("runtimeDeliveries", { version: 1, records: [] });
|
|
214
|
+
let changed = false;
|
|
215
|
+
const pendingUpdates = [];
|
|
216
|
+
const records = disk.records.map((candidate) => {
|
|
217
|
+
const current = agent.records.get(candidate.deliveryId);
|
|
218
|
+
if (!current)
|
|
219
|
+
return candidate;
|
|
220
|
+
if (candidate.status === "consumed") {
|
|
221
|
+
if (current.status !== "consumed") {
|
|
222
|
+
agent.records.set(candidate.deliveryId, candidate);
|
|
223
|
+
consumed.push(candidate);
|
|
224
|
+
}
|
|
225
|
+
return candidate;
|
|
226
|
+
}
|
|
227
|
+
if (current.status !== "accepted" || candidate.status !== "accepted" || !inboxIds.has(candidate.messageId)) {
|
|
228
|
+
return candidate;
|
|
229
|
+
}
|
|
230
|
+
const next = { ...candidate, status: "pending", updatedAt: now(),
|
|
231
|
+
reason: TURN_END_RETRY_REASON, retryable: true };
|
|
232
|
+
changed = true;
|
|
233
|
+
pendingUpdates.push(next);
|
|
234
|
+
return next;
|
|
235
|
+
});
|
|
236
|
+
if (changed) {
|
|
237
|
+
store.writeJson("runtimeDeliveries", { ...disk, records });
|
|
238
|
+
for (const next of pendingUpdates) {
|
|
239
|
+
agent.records.set(next.deliveryId, next);
|
|
240
|
+
deferred.push(next);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
log("turn-end Inbox reconciliation failed", String(error));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
emitConsumed(agent, consumed);
|
|
250
|
+
for (const record of deferred)
|
|
251
|
+
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
252
|
+
deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason: TURN_END_RETRY_REASON });
|
|
253
|
+
};
|
|
254
|
+
// Production callers persist the canonical Inbox before calling deliver(). If a
|
|
255
|
+
// concurrent drain wins between that append and ledger creation, close only the
|
|
256
|
+
// record created/observed by this call under the same Inbox lock.
|
|
257
|
+
const reconcileAbsentCanonical = (agent, record) => {
|
|
258
|
+
const store = agent.stateStore;
|
|
259
|
+
if (!store?.readNdjson || !isActiveDelivery(record.status))
|
|
260
|
+
return;
|
|
261
|
+
let consumed = null;
|
|
262
|
+
try {
|
|
263
|
+
store.withInboxTransaction(() => {
|
|
264
|
+
const present = store.readNdjson("inbox")
|
|
265
|
+
.some((row) => row?.message_id === record.messageId);
|
|
266
|
+
if (present)
|
|
267
|
+
return;
|
|
268
|
+
const disk = store.readJson("runtimeDeliveries", { version: 1, records: [] });
|
|
269
|
+
let changed = false;
|
|
270
|
+
const records = disk.records.map((candidate) => {
|
|
271
|
+
if (candidate.deliveryId !== record.deliveryId || !isActiveDelivery(candidate.status))
|
|
272
|
+
return candidate;
|
|
273
|
+
changed = true;
|
|
274
|
+
consumed = { ...candidate, status: "consumed", updatedAt: now() };
|
|
275
|
+
return consumed;
|
|
276
|
+
});
|
|
277
|
+
if (changed)
|
|
278
|
+
store.writeJson("runtimeDeliveries", { ...disk, records });
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
log("post-delivery Inbox reconciliation failed", String(error));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (consumed) {
|
|
286
|
+
agent.records.set(record.deliveryId, consumed);
|
|
287
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
|
|
288
|
+
messageId: record.messageId, status: "consumed" });
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const resultFor = (agent, record, result) => {
|
|
292
|
+
if (result.status === "accepted") {
|
|
293
|
+
delete record.reason;
|
|
294
|
+
delete record.retryable;
|
|
295
|
+
const finalRecord = setRecord(agent, record, "accepted");
|
|
296
|
+
if (finalRecord.status !== "consumed")
|
|
297
|
+
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
298
|
+
deliveryId: record.deliveryId, messageId: record.messageId, status: "accepted" });
|
|
299
|
+
return { status: "accepted", deliveryId: record.deliveryId };
|
|
300
|
+
}
|
|
301
|
+
const retryable = result.status === "deferred" || result.retryable;
|
|
302
|
+
record.reason = result.reason;
|
|
303
|
+
record.retryable = retryable;
|
|
304
|
+
const finalRecord = setRecord(agent, record, retryable ? "pending" : "error");
|
|
305
|
+
if (finalRecord.status === "consumed")
|
|
306
|
+
return { status: "accepted", deliveryId: record.deliveryId };
|
|
307
|
+
const status = retryable ? "deferred" : "error";
|
|
308
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId, messageId: record.messageId, status, reason: result.reason });
|
|
309
|
+
return retryable
|
|
310
|
+
? { status: "deferred", deliveryId: record.deliveryId, reason: result.reason }
|
|
311
|
+
: { status: "error", deliveryId: record.deliveryId, reason: result.reason, retryable: false };
|
|
312
|
+
};
|
|
313
|
+
const submit = async (agent, record, busy) => {
|
|
314
|
+
agent.submitting = true;
|
|
315
|
+
if (!busy)
|
|
316
|
+
agent.busy = true; // Reserve the turn before prompt() can yield.
|
|
317
|
+
try {
|
|
318
|
+
const session = await ensureSession(agent);
|
|
319
|
+
const submittingRecord = setRecord(agent, record, "submitting");
|
|
320
|
+
if (submittingRecord.status === "consumed") {
|
|
321
|
+
if (!busy)
|
|
322
|
+
agent.busy = false;
|
|
323
|
+
return { status: "accepted", deliveryId: record.deliveryId };
|
|
324
|
+
}
|
|
325
|
+
const result = await (busy ? session.busyInput(record.input) : session.prompt(record.input));
|
|
326
|
+
if (agent.session !== session || agent.stopped) {
|
|
327
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason: "runtime session replaced before input result" };
|
|
328
|
+
}
|
|
329
|
+
if (result.status !== "accepted" && !busy)
|
|
330
|
+
agent.busy = false;
|
|
331
|
+
if (result.status === "accepted")
|
|
332
|
+
markSessionStable(agent, session);
|
|
333
|
+
return resultFor(agent, record, result);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (!busy)
|
|
337
|
+
agent.busy = false;
|
|
338
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
339
|
+
record.reason = reason;
|
|
340
|
+
record.retryable = true;
|
|
341
|
+
setRecord(agent, record, "pending");
|
|
342
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason });
|
|
343
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason };
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
agent.submitting = false;
|
|
347
|
+
if (agent.retryAfterSubmit && !agent.stopped) {
|
|
348
|
+
agent.retryAfterSubmit = false;
|
|
349
|
+
queueMicrotask(() => { void retryPending(agent); });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const retryPending = async (agent) => {
|
|
354
|
+
reconcileExternalConsumption(agent);
|
|
355
|
+
if (agent.submitting) {
|
|
356
|
+
agent.retryAfterSubmit = true;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (agent.busy || agent.turnInProgress || !agent.session)
|
|
360
|
+
return;
|
|
361
|
+
const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
|
|
362
|
+
if (record)
|
|
363
|
+
await submit(agent, record, false);
|
|
364
|
+
};
|
|
365
|
+
const scheduleRecreate = (agent, reason) => {
|
|
366
|
+
if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
|
|
367
|
+
return;
|
|
368
|
+
if (agent.recreateAttempts >= retryPolicy.maxAttempts) {
|
|
369
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
|
|
370
|
+
error: `runtime recreation exhausted after ${agent.recreateAttempts} attempts: ${reason}` });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const attempt = ++agent.recreateAttempts;
|
|
374
|
+
const delay = Math.min(retryPolicy.maxDelayMs, retryPolicy.baseDelayMs * 2 ** (attempt - 1));
|
|
375
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
|
|
376
|
+
error: `runtime recreate attempt ${attempt}/${retryPolicy.maxAttempts} in ${delay}ms: ${reason}` });
|
|
377
|
+
agent.retryTimer = setTimeout(() => {
|
|
378
|
+
agent.retryTimer = null;
|
|
379
|
+
if (agent.stopped)
|
|
380
|
+
return;
|
|
381
|
+
void ensureSession(agent).then(() => {
|
|
382
|
+
return retryPending(agent);
|
|
383
|
+
}).catch((error) => {
|
|
384
|
+
const readiness = error instanceof runtime_readiness_js_1.RuntimePrerequisiteError ? error.readiness
|
|
385
|
+
: (0, runtime_readiness_js_1.classifyRuntimePrerequisite)(agent.adapter.id, error);
|
|
386
|
+
agent.readiness = readiness;
|
|
387
|
+
const nextReason = error instanceof Error ? error.message : String(error);
|
|
388
|
+
if (readiness.state === "unavailable")
|
|
389
|
+
scheduleRecreate(agent, nextReason);
|
|
390
|
+
else {
|
|
391
|
+
agent.disabledReason = nextReason;
|
|
392
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error", error: nextReason, readiness });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}, delay);
|
|
396
|
+
agent.retryTimer.unref?.();
|
|
397
|
+
};
|
|
398
|
+
const replaceSession = (agent, session, reason) => {
|
|
399
|
+
if (agent.session !== session || agent.stopped)
|
|
400
|
+
return;
|
|
401
|
+
agent.session = null;
|
|
402
|
+
agent.busy = false;
|
|
403
|
+
agent.submitting = false;
|
|
404
|
+
agent.generation += 1;
|
|
405
|
+
agent.recreateReason = reason;
|
|
406
|
+
if (agent.stabilityTimer)
|
|
407
|
+
clearTimeout(agent.stabilityTimer);
|
|
408
|
+
agent.stabilityTimer = null;
|
|
409
|
+
void session.close(`replace after ${reason}`).catch((error) => log("runtime close after replacement failed", String(error)));
|
|
410
|
+
for (const record of agent.records.values())
|
|
411
|
+
if (isActiveDelivery(record.status))
|
|
412
|
+
record.status = "pending";
|
|
413
|
+
emitConsumed(agent, persist(agent));
|
|
414
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error", error: reason });
|
|
415
|
+
scheduleRecreate(agent, reason);
|
|
416
|
+
};
|
|
417
|
+
const recoverConfiguration = (agent, session, message) => {
|
|
418
|
+
if (agent.session !== session || agent.stopped)
|
|
419
|
+
return;
|
|
420
|
+
agent.disabledReason = `runtime configuration recovery in progress: ${message}`;
|
|
421
|
+
agent.session = null;
|
|
422
|
+
agent.busy = false;
|
|
423
|
+
agent.submitting = false;
|
|
424
|
+
agent.generation += 1;
|
|
425
|
+
if (agent.stabilityTimer)
|
|
426
|
+
clearTimeout(agent.stabilityTimer);
|
|
427
|
+
agent.stabilityTimer = null;
|
|
428
|
+
for (const record of agent.records.values()) {
|
|
429
|
+
if (!isActiveDelivery(record.status))
|
|
430
|
+
continue;
|
|
431
|
+
setRecord(agent, record, "pending");
|
|
432
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
|
|
433
|
+
messageId: record.messageId, status: "deferred", reason: message });
|
|
434
|
+
}
|
|
435
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error", error: message });
|
|
436
|
+
const recovery = (async () => {
|
|
437
|
+
try {
|
|
438
|
+
await session.close("runtime configuration error");
|
|
439
|
+
const result = agent.adapter.recoverConfigurationError
|
|
440
|
+
? await agent.adapter.recoverConfigurationError(message)
|
|
441
|
+
: { recovered: false, reason: "runtime adapter does not support automatic configuration recovery" };
|
|
442
|
+
if (agent.stopped)
|
|
443
|
+
return;
|
|
444
|
+
if (!result.recovered) {
|
|
445
|
+
agent.disabledReason = `${message}\n${result.reason}`;
|
|
446
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error", error: agent.disabledReason });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
agent.disabledReason = null;
|
|
450
|
+
agent.recreateAttempts = 0;
|
|
451
|
+
agent.recreateReason = result.reason;
|
|
452
|
+
scheduleRecreate(agent, result.reason);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (agent.stopped)
|
|
456
|
+
return;
|
|
457
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
458
|
+
agent.disabledReason = `${message}\nCodex update failed: ${reason}`;
|
|
459
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error", error: agent.disabledReason });
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
agent.configurationRecovery = null;
|
|
463
|
+
}
|
|
464
|
+
})();
|
|
465
|
+
agent.configurationRecovery = recovery;
|
|
466
|
+
};
|
|
467
|
+
const observe = (agent, session, event) => {
|
|
468
|
+
if (agent.session !== session)
|
|
469
|
+
return; // Ignore late output from a replaced child.
|
|
470
|
+
// Keep the trace parent published until authoritative Inbox reconciliation
|
|
471
|
+
// has observed any direct CLI poll completed by this turn.
|
|
472
|
+
if (event.type !== "turn-end")
|
|
473
|
+
telemetry?.runtimeEvent(agent.config.agentId, event);
|
|
474
|
+
emit({ type: "runtime", agentId: agent.config.agentId, event });
|
|
475
|
+
if (event.type === "session-init") {
|
|
476
|
+
agent.config.sessionId = event.sessionId;
|
|
477
|
+
emit({ type: "session", agentId: agent.config.agentId, runtime: agent.adapter.id, sessionId: event.sessionId, launchId: agent.launchId,
|
|
478
|
+
...(event.model ? { model: event.model } : {}), ...(event.reasoningEffort ? { reasoningEffort: event.reasoningEffort } : {}) });
|
|
479
|
+
void retryPending(agent);
|
|
480
|
+
}
|
|
481
|
+
else if (event.type === "turn-start") {
|
|
482
|
+
agent.busy = true;
|
|
483
|
+
agent.turnInProgress = true;
|
|
484
|
+
agent.turnHadFailure = false;
|
|
485
|
+
agent.turnHadAuthenticatedOutput = false;
|
|
486
|
+
markSessionStable(agent, session);
|
|
487
|
+
emit({ type: "activity", agentId: agent.config.agentId, activity: "working", activityKind: "working", detailKind: "turn_started" });
|
|
488
|
+
}
|
|
489
|
+
else if (event.type === "turn-end") {
|
|
490
|
+
const recoveredAuthentication = agent.authFailureActive && agent.turnInProgress
|
|
491
|
+
&& agent.turnHadAuthenticatedOutput && !agent.turnHadFailure;
|
|
492
|
+
agent.turnInProgress = false;
|
|
493
|
+
agent.busy = false;
|
|
494
|
+
emit({ type: "activity", agentId: agent.config.agentId, activity: "idle", activityKind: "idle", detailKind: "turn_ended" });
|
|
495
|
+
reconcileAcceptedAtTurnEnd(agent);
|
|
496
|
+
telemetry?.runtimeEvent(agent.config.agentId, event);
|
|
497
|
+
if (recoveredAuthentication) {
|
|
498
|
+
agent.authFailureActive = false;
|
|
499
|
+
const prior = agent.readiness;
|
|
500
|
+
agent.readiness = {
|
|
501
|
+
runtime: agent.adapter.id,
|
|
502
|
+
state: "ready",
|
|
503
|
+
...(prior?.executable ? { executable: prior.executable } : {}),
|
|
504
|
+
...(prior?.version ? { version: prior.version } : {}),
|
|
505
|
+
};
|
|
506
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "active", readiness: agent.readiness });
|
|
507
|
+
}
|
|
508
|
+
void retryPending(agent);
|
|
509
|
+
}
|
|
510
|
+
else if (event.type === "activity") {
|
|
511
|
+
if (agent.turnInProgress && event.activity !== "internal")
|
|
512
|
+
agent.turnHadAuthenticatedOutput = true;
|
|
513
|
+
emit({ type: "activity", agentId: agent.config.agentId, activity: event.activity, activityKind: event.activity });
|
|
514
|
+
}
|
|
515
|
+
else if (event.type === "input-error") {
|
|
516
|
+
const record = event.inputId
|
|
517
|
+
? agent.records.get(event.inputId) ?? [...agent.records.values()].find((candidate) => candidate.input.inputId === event.inputId)
|
|
518
|
+
: undefined;
|
|
519
|
+
if (!record || record.status === "consumed" || record.status === "error")
|
|
520
|
+
return;
|
|
521
|
+
agent.turnHadFailure = true;
|
|
522
|
+
if (event.willRetry) {
|
|
523
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
|
|
524
|
+
messageId: record.messageId, status: "deferred", reason: event.message });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const submittedWhileBusy = record.input.kind === "inbox_update" || record.input.kind === "inbox-update";
|
|
528
|
+
if (!submittedWhileBusy)
|
|
529
|
+
agent.busy = false;
|
|
530
|
+
record.reason = event.message;
|
|
531
|
+
record.retryable = event.retryable;
|
|
532
|
+
const finalRecord = setRecord(agent, record, event.retryable ? "pending" : "error");
|
|
533
|
+
if (finalRecord.status !== "consumed")
|
|
534
|
+
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
535
|
+
deliveryId: record.deliveryId, messageId: record.messageId,
|
|
536
|
+
status: event.retryable ? "deferred" : "error", reason: event.message });
|
|
537
|
+
if (event.errorCategory === "auth" && !event.retryable) {
|
|
538
|
+
const readiness = (0, runtime_readiness_js_1.providerAuthenticationFailureReadiness)(agent.adapter.id, event.upstream?.provider);
|
|
539
|
+
agent.authFailureActive = true;
|
|
540
|
+
agent.readiness = {
|
|
541
|
+
...readiness,
|
|
542
|
+
...(agent.readiness?.executable ? { executable: agent.readiness.executable } : {}),
|
|
543
|
+
...(agent.readiness?.version ? { version: agent.readiness.version } : {}),
|
|
544
|
+
};
|
|
545
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
|
|
546
|
+
error: `${agent.readiness.reason} ${agent.readiness.nextAction}`, readiness: agent.readiness });
|
|
547
|
+
}
|
|
548
|
+
if (event.retryable && !agent.busy && !agent.turnInProgress)
|
|
549
|
+
void retryPending(agent);
|
|
550
|
+
}
|
|
551
|
+
else if (event.type === "configuration-error") {
|
|
552
|
+
recoverConfiguration(agent, session, event.message);
|
|
553
|
+
}
|
|
554
|
+
else if (event.type === "error" || event.type === "closed") {
|
|
555
|
+
replaceSession(agent, session, event.type === "error" ? event.message : `runtime closed (${event.code ?? event.signal ?? "unknown"})`);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const ensureSession = async (agent) => {
|
|
559
|
+
if (agent.disabledReason)
|
|
560
|
+
throw new Error(agent.disabledReason);
|
|
561
|
+
if (agent.session)
|
|
562
|
+
return agent.session;
|
|
563
|
+
if (agent.starting)
|
|
564
|
+
return agent.starting;
|
|
565
|
+
const probeEnv = runtimeEnv(agent.config);
|
|
566
|
+
await assertOfficialCliReady(agent.config, probeEnv);
|
|
567
|
+
const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
|
|
568
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
569
|
+
env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
|
|
570
|
+
LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
|
|
571
|
+
: { runtime: agent.adapter.id, state: "ready" };
|
|
572
|
+
if (!agent.authFailureActive)
|
|
573
|
+
agent.readiness = readiness;
|
|
574
|
+
if (readiness.state !== "ready")
|
|
575
|
+
throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
|
|
576
|
+
const standingPrompt = options.promptBuilder.build({
|
|
577
|
+
agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
|
|
578
|
+
description: agent.config.description || "", runtime: agent.adapter.id,
|
|
579
|
+
cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
|
|
580
|
+
});
|
|
581
|
+
const generation = ++agent.generation;
|
|
582
|
+
let completedSession = null;
|
|
583
|
+
agent.starting = agent.adapter.createSession({
|
|
584
|
+
agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
|
|
585
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
586
|
+
resumeSessionId: agent.config.sessionId || null, standingPrompt,
|
|
587
|
+
env: runtimeEnv(agent.config, `${agent.launchId}:${generation}`),
|
|
588
|
+
}).then((session) => {
|
|
589
|
+
completedSession = session;
|
|
590
|
+
if (agent.stopped || generation !== agent.generation) {
|
|
591
|
+
void session.close("stale creation");
|
|
592
|
+
throw new Error("stale runtime session creation");
|
|
593
|
+
}
|
|
594
|
+
agent.session = session;
|
|
595
|
+
session.subscribe((event) => observe(agent, session, event));
|
|
596
|
+
if (agent.session !== session)
|
|
597
|
+
return session;
|
|
598
|
+
if (session.sessionId) {
|
|
599
|
+
agent.config.sessionId = session.sessionId;
|
|
600
|
+
emit({ type: "session", agentId: agent.config.agentId, runtime: agent.adapter.id, sessionId: session.sessionId, launchId: agent.launchId,
|
|
601
|
+
...(session.effectiveModel ? { model: session.effectiveModel } : {}),
|
|
602
|
+
...(session.effectiveReasoningEffort ? { reasoningEffort: session.effectiveReasoningEffort } : {}) });
|
|
603
|
+
}
|
|
604
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: agent.authFailureActive ? "error" : "active",
|
|
605
|
+
...(agent.authFailureActive ? { error: `${agent.readiness?.reason} ${agent.readiness?.nextAction}` } : {}),
|
|
606
|
+
readiness: agent.readiness ?? readiness });
|
|
607
|
+
agent.stabilityTimer = setTimeout(() => markSessionStable(agent, session), retryPolicy.stableWindowMs);
|
|
608
|
+
agent.stabilityTimer.unref?.();
|
|
609
|
+
return session;
|
|
610
|
+
}).finally(() => {
|
|
611
|
+
agent.starting = null;
|
|
612
|
+
if (completedSession && !agent.session && !agent.stopped) {
|
|
613
|
+
scheduleRecreate(agent, agent.recreateReason || "runtime closed during session initialization");
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
return agent.starting;
|
|
617
|
+
};
|
|
618
|
+
return {
|
|
619
|
+
async probe(config) {
|
|
620
|
+
const adapter = options.adapterFor(config.runtime);
|
|
621
|
+
const env = runtimeEnv(config);
|
|
622
|
+
await assertOfficialCliReady(config, env);
|
|
623
|
+
return adapter.probe ? adapter.probe({ agentId: config.agentId, workspaceDir: config.workspaceDir, stateDir: config.stateDir,
|
|
624
|
+
env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
|
|
625
|
+
LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...env } })
|
|
626
|
+
: { runtime: adapter.id, state: "ready" };
|
|
627
|
+
},
|
|
628
|
+
async stage(config) {
|
|
629
|
+
const previous = managed.get(config.agentId);
|
|
630
|
+
if (!previous)
|
|
631
|
+
throw new Error(`cannot stage unknown runtime Agent: ${config.agentId}`);
|
|
632
|
+
if (previous.busy || previous.submitting || previous.starting) {
|
|
633
|
+
throw new Error(`Agent ${config.agentId} is not idle for runtime staging`);
|
|
634
|
+
}
|
|
635
|
+
const adapter = options.adapterFor(config.runtime);
|
|
636
|
+
const stageEnv = runtimeEnv(config, `${node_crypto_1.default.randomUUID()}:staged`);
|
|
637
|
+
await assertOfficialCliReady(config, stageEnv);
|
|
638
|
+
const readiness = adapter.probe ? await adapter.probe({ agentId: config.agentId, workspaceDir: config.workspaceDir, stateDir: config.stateDir,
|
|
639
|
+
env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
|
|
640
|
+
LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...stageEnv } })
|
|
641
|
+
: { runtime: adapter.id, state: "ready" };
|
|
642
|
+
if (readiness.state !== "ready")
|
|
643
|
+
throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
|
|
644
|
+
const standingPrompt = options.promptBuilder.build({
|
|
645
|
+
agentId: config.agentId, name: config.displayName || config.name,
|
|
646
|
+
description: config.description || "", runtime: adapter.id,
|
|
647
|
+
cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
|
|
648
|
+
});
|
|
649
|
+
const session = await adapter.createSession({
|
|
650
|
+
agentId: config.agentId, model: config.model, reasoningEffort: config.effort || null,
|
|
651
|
+
workspaceDir: config.workspaceDir, stateDir: config.stateDir,
|
|
652
|
+
resumeSessionId: config.sessionId || null, standingPrompt,
|
|
653
|
+
env: stageEnv,
|
|
654
|
+
});
|
|
655
|
+
let state = "staged";
|
|
656
|
+
const rollback = async (reason) => {
|
|
657
|
+
if (state !== "staged")
|
|
658
|
+
return;
|
|
659
|
+
state = "rolled_back";
|
|
660
|
+
await session.close(reason);
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
readiness,
|
|
664
|
+
async commit() {
|
|
665
|
+
if (state !== "staged")
|
|
666
|
+
throw new Error(`runtime candidate is ${state}`);
|
|
667
|
+
const current = managed.get(config.agentId);
|
|
668
|
+
if (current !== previous || previous.busy || previous.submitting || previous.starting) {
|
|
669
|
+
await rollback("runtime candidate commit precondition changed");
|
|
670
|
+
throw new Error(`Agent ${config.agentId} changed or became busy during runtime staging`);
|
|
671
|
+
}
|
|
672
|
+
state = "committed";
|
|
673
|
+
previous.stopped = true;
|
|
674
|
+
previous.generation += 1;
|
|
675
|
+
if (previous.poller)
|
|
676
|
+
clearInterval(previous.poller);
|
|
677
|
+
if (previous.retryTimer)
|
|
678
|
+
clearTimeout(previous.retryTimer);
|
|
679
|
+
if (previous.stabilityTimer)
|
|
680
|
+
clearTimeout(previous.stabilityTimer);
|
|
681
|
+
const candidate = {
|
|
682
|
+
...previous, config, adapter, session, launchId: node_crypto_1.default.randomUUID(), busy: false, submitting: false,
|
|
683
|
+
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
|
|
684
|
+
recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
|
|
685
|
+
disabledReason: null, configurationRecovery: null,
|
|
686
|
+
readiness: previous.authFailureActive ? previous.readiness : readiness,
|
|
687
|
+
};
|
|
688
|
+
managed.set(config.agentId, candidate);
|
|
689
|
+
session.subscribe((event) => observe(candidate, session, event));
|
|
690
|
+
if (session.sessionId) {
|
|
691
|
+
candidate.config.sessionId = session.sessionId;
|
|
692
|
+
emit({ type: "session", agentId: config.agentId, runtime: adapter.id, sessionId: session.sessionId,
|
|
693
|
+
launchId: candidate.launchId, ...(session.effectiveModel ? { model: session.effectiveModel } : {}),
|
|
694
|
+
...(session.effectiveReasoningEffort ? { reasoningEffort: session.effectiveReasoningEffort } : {}) });
|
|
695
|
+
}
|
|
696
|
+
candidate.poller = setInterval(() => reconcileExternalConsumption(candidate), 250);
|
|
697
|
+
candidate.poller.unref?.();
|
|
698
|
+
if (!candidate.authFailureActive) {
|
|
699
|
+
emit({ type: "agent-status", agentId: config.agentId, status: "active", readiness: candidate.readiness ?? readiness });
|
|
700
|
+
}
|
|
701
|
+
await previous.session?.close("runtime candidate committed")
|
|
702
|
+
.catch((error) => log("previous runtime close after candidate commit failed", String(error)));
|
|
703
|
+
},
|
|
704
|
+
rollback,
|
|
705
|
+
};
|
|
706
|
+
},
|
|
707
|
+
isBusy(agentId) { const agent = managed.get(agentId); return Boolean(agent?.busy || agent?.submitting || agent?.starting); },
|
|
708
|
+
async resetSession(agentId) {
|
|
709
|
+
const agent = managed.get(agentId);
|
|
710
|
+
if (!agent || agent.stopped)
|
|
711
|
+
throw new RuntimeSessionResetError("unknown_agent", `unknown runtime Agent: ${agentId}`);
|
|
712
|
+
if (agent.busy || agent.turnInProgress || agent.submitting || agent.starting) {
|
|
713
|
+
throw new RuntimeSessionResetError("agent_busy", `Agent ${agentId} is not idle`);
|
|
714
|
+
}
|
|
715
|
+
const countPending = () => agent.stateStore
|
|
716
|
+
? agent.stateStore.withInboxTransaction(() => agent.stateStore.readNdjson?.("inbox").length ?? 0)
|
|
717
|
+
: [...agent.records.values()].filter((record) => isActiveDelivery(record.status)).length;
|
|
718
|
+
const pendingCount = countPending();
|
|
719
|
+
if (pendingCount > 0)
|
|
720
|
+
throw new RuntimeSessionResetError("inbox_backlog", `Agent ${agentId} has unconsumed Inbox backlog`, pendingCount);
|
|
721
|
+
const oldSession = agent.session;
|
|
722
|
+
if (!oldSession)
|
|
723
|
+
throw new RuntimeSessionResetError("agent_busy", `Agent ${agentId} Runtime session is unavailable`);
|
|
724
|
+
const oldSessionId = oldSession.sessionId;
|
|
725
|
+
const probeEnv = runtimeEnv(agent.config, `${agent.launchId}:reset`);
|
|
726
|
+
await assertOfficialCliReady(agent.config, probeEnv);
|
|
727
|
+
const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
|
|
728
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
729
|
+
env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
|
|
730
|
+
LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
|
|
731
|
+
: { runtime: agent.adapter.id, state: "ready" };
|
|
732
|
+
if (readiness.state !== "ready")
|
|
733
|
+
throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
|
|
734
|
+
const standingPrompt = options.promptBuilder.build({
|
|
735
|
+
agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
|
|
736
|
+
description: agent.config.description || "", runtime: agent.adapter.id,
|
|
737
|
+
cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
|
|
738
|
+
});
|
|
739
|
+
const create = agent.adapter.createSession({
|
|
740
|
+
agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
|
|
741
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
742
|
+
resumeSessionId: null, standingPrompt, env: probeEnv,
|
|
743
|
+
});
|
|
744
|
+
agent.starting = create;
|
|
745
|
+
let fresh;
|
|
746
|
+
try {
|
|
747
|
+
fresh = await create;
|
|
748
|
+
}
|
|
749
|
+
finally {
|
|
750
|
+
if (agent.starting === create)
|
|
751
|
+
agent.starting = null;
|
|
752
|
+
}
|
|
753
|
+
const commit = () => {
|
|
754
|
+
if (agent.session !== oldSession || agent.busy || agent.turnInProgress || agent.submitting || agent.stopped) {
|
|
755
|
+
throw new RuntimeSessionResetError("agent_busy", `Agent ${agentId} changed during reset`);
|
|
756
|
+
}
|
|
757
|
+
const pendingAfterCreate = agent.stateStore?.readNdjson?.("inbox").length
|
|
758
|
+
?? [...agent.records.values()].filter((record) => isActiveDelivery(record.status)).length;
|
|
759
|
+
if (pendingAfterCreate > 0) {
|
|
760
|
+
throw new RuntimeSessionResetError("inbox_backlog", `Agent ${agentId} received Inbox backlog during reset`, pendingAfterCreate);
|
|
761
|
+
}
|
|
762
|
+
agent.generation += 1;
|
|
763
|
+
agent.launchId = node_crypto_1.default.randomUUID();
|
|
764
|
+
agent.config.sessionId = null;
|
|
765
|
+
agent.session = fresh;
|
|
766
|
+
agent.readiness = readiness;
|
|
767
|
+
agent.disabledReason = null;
|
|
768
|
+
};
|
|
769
|
+
try {
|
|
770
|
+
if (agent.stateStore)
|
|
771
|
+
agent.stateStore.withInboxTransaction(commit);
|
|
772
|
+
else
|
|
773
|
+
commit();
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
await fresh.close("fresh session reset precondition changed");
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
fresh.subscribe((event) => observe(agent, fresh, event));
|
|
780
|
+
if (fresh.sessionId) {
|
|
781
|
+
agent.config.sessionId = fresh.sessionId;
|
|
782
|
+
emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
|
|
783
|
+
...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
|
|
784
|
+
...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
|
|
785
|
+
}
|
|
786
|
+
emit({ type: "agent-status", agentId, status: "active", readiness });
|
|
787
|
+
await oldSession.close("fresh session reset committed")
|
|
788
|
+
.catch((error) => log("previous runtime close after fresh reset failed", String(error)));
|
|
789
|
+
return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
|
|
790
|
+
runtimeReady: true, pendingCount: 0, sessionId: fresh.sessionId };
|
|
791
|
+
},
|
|
792
|
+
async start(configs) {
|
|
793
|
+
let activeCount = 0;
|
|
794
|
+
let recoveringCount = 0;
|
|
795
|
+
const startupFailures = [];
|
|
796
|
+
for (const config of configs) {
|
|
797
|
+
if (managed.has(config.agentId)) {
|
|
798
|
+
activeCount += 1;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const stateStore = options.stateStoreFor?.(config.agentId);
|
|
802
|
+
const persisted = stateStore
|
|
803
|
+
? stateStore.withInboxTransaction(() => {
|
|
804
|
+
const deliveryFile = stateStore.readJson("runtimeDeliveries", { version: 1, records: [] });
|
|
805
|
+
if (!stateStore.readNdjson)
|
|
806
|
+
return deliveryFile;
|
|
807
|
+
const inboxIds = new Set(stateStore.readNdjson("inbox").flatMap((row) => typeof row?.message_id === "string" ? [row.message_id] : []));
|
|
808
|
+
let changed = false;
|
|
809
|
+
const records = deliveryFile.records.map((record) => {
|
|
810
|
+
const synthetic = /^(?:redeliver_|rem_|interaction_)/.test(record.messageId);
|
|
811
|
+
if (!synthetic || !isActiveDelivery(record.status) || inboxIds.has(record.messageId))
|
|
812
|
+
return record;
|
|
813
|
+
changed = true;
|
|
814
|
+
return { ...record, status: "consumed", updatedAt: now() };
|
|
815
|
+
});
|
|
816
|
+
const migrated = changed ? { ...deliveryFile, records } : deliveryFile;
|
|
817
|
+
if (changed)
|
|
818
|
+
stateStore.writeJson("runtimeDeliveries", migrated);
|
|
819
|
+
return migrated;
|
|
820
|
+
})
|
|
821
|
+
: readDeliveryFile(config.stateDir ? node_path_1.default.join(config.stateDir, "runtime-deliveries.json") : null);
|
|
822
|
+
const agent = { config, adapter: options.adapterFor(config.runtime), session: null,
|
|
823
|
+
launchId: node_crypto_1.default.randomUUID(), busy: false, submitting: false, starting: null, retryAfterSubmit: false,
|
|
824
|
+
records: new Map(persisted.records.map((record) => [record.deliveryId, record])),
|
|
825
|
+
byMessage: new Map(persisted.records.map((record) => [record.messageId, record.deliveryId])),
|
|
826
|
+
generation: 0, poller: null, retryTimer: null, recreateAttempts: 0,
|
|
827
|
+
stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
|
|
828
|
+
configurationRecovery: null, stateStore, readiness: null,
|
|
829
|
+
turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
|
|
830
|
+
for (const record of agent.records.values())
|
|
831
|
+
if (isActiveDelivery(record.status))
|
|
832
|
+
record.status = "pending";
|
|
833
|
+
emitConsumed(agent, persist(agent));
|
|
834
|
+
managed.set(config.agentId, agent);
|
|
835
|
+
try {
|
|
836
|
+
await ensureSession(agent);
|
|
837
|
+
activeCount += 1;
|
|
838
|
+
await retryPending(agent);
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
842
|
+
const transient = error instanceof runtime_readiness_js_1.RuntimePrerequisiteError && error.readiness.state === "unavailable";
|
|
843
|
+
agent.disabledReason = transient ? null : reason;
|
|
844
|
+
if (transient) {
|
|
845
|
+
recoveringCount += 1;
|
|
846
|
+
scheduleRecreate(agent, reason);
|
|
847
|
+
}
|
|
848
|
+
startupFailures.push(`${config.agentId}: ${reason}`);
|
|
849
|
+
emit({ type: "agent-status", agentId: config.agentId, status: "error", error: reason,
|
|
850
|
+
...(error instanceof runtime_readiness_js_1.RuntimePrerequisiteError ? { readiness: error.readiness } : {}) });
|
|
851
|
+
}
|
|
852
|
+
agent.poller = setInterval(() => reconcileExternalConsumption(agent), 250);
|
|
853
|
+
agent.poller.unref?.();
|
|
854
|
+
}
|
|
855
|
+
if (configs.length > 0 && activeCount === 0 && recoveringCount === 0) {
|
|
856
|
+
throw new Error(`No runtime Agent started: ${startupFailures.join("; ")}`);
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
async deliver(agentId, envelope) {
|
|
860
|
+
const messageId = String(envelope.message_id || envelope.seq || node_crypto_1.default.randomUUID());
|
|
861
|
+
const agent = managed.get(agentId);
|
|
862
|
+
if (!agent) {
|
|
863
|
+
telemetry?.delivery(agentId, messageId, "error");
|
|
864
|
+
throw new Error(`unknown runtime Agent: ${agentId}`);
|
|
865
|
+
}
|
|
866
|
+
reconcileExternalConsumption(agent);
|
|
867
|
+
const existingId = agent.byMessage.get(messageId);
|
|
868
|
+
if (existingId) {
|
|
869
|
+
const existing = agent.records.get(existingId);
|
|
870
|
+
if (existing?.status === "error") {
|
|
871
|
+
existing.input.attempt += 1;
|
|
872
|
+
delete existing.reason;
|
|
873
|
+
delete existing.retryable;
|
|
874
|
+
setRecord(agent, existing, "pending");
|
|
875
|
+
const receipt = await (telemetry?.phase(messageId, "runtime.deliver", api_1.SpanKind.PRODUCER, () => submit(agent, existing, agent.busy || agent.submitting)) ?? submit(agent, existing, agent.busy || agent.submitting));
|
|
876
|
+
telemetry?.delivery(agentId, messageId, receipt.status);
|
|
877
|
+
reconcileAbsentCanonical(agent, agent.records.get(existingId) ?? existing);
|
|
878
|
+
return receipt;
|
|
879
|
+
}
|
|
880
|
+
if (existing)
|
|
881
|
+
reconcileAbsentCanonical(agent, existing);
|
|
882
|
+
telemetry?.delivery(agentId, messageId, "duplicate");
|
|
883
|
+
return { status: "duplicate", deliveryId: existingId };
|
|
884
|
+
}
|
|
885
|
+
if ([...agent.records.values()].filter((record) => isActiveDelivery(record.status)).length >= MAX_DELIVERIES) {
|
|
886
|
+
const deliveryId = `overflow-${node_crypto_1.default.createHash("sha256").update(messageId).digest("hex").slice(0, 24)}`;
|
|
887
|
+
telemetry?.delivery(agentId, messageId, "deferred");
|
|
888
|
+
return { status: "deferred", deliveryId, reason: `runtime delivery backlog limit ${MAX_DELIVERIES} reached` };
|
|
889
|
+
}
|
|
890
|
+
const deliveryId = node_crypto_1.default.randomUUID();
|
|
891
|
+
const busy = agent.busy || agent.submitting;
|
|
892
|
+
const input = { inputId: deliveryId, deliveryId, kind: busy ? "inbox_update" : "wake",
|
|
893
|
+
text: options.promptBuilder.buildInboxNotice({ busy, count: 1, deliveryId,
|
|
894
|
+
...(typeof envelope.target === "string" ? { target: envelope.target } : {}),
|
|
895
|
+
...(typeof envelope.wake_reason === "string" ? { wakeReason: envelope.wake_reason } : {}) }), attempt: 0 };
|
|
896
|
+
const record = { deliveryId, messageId, status: "pending", input, updatedAt: now() };
|
|
897
|
+
agent.records.set(deliveryId, record);
|
|
898
|
+
agent.byMessage.set(messageId, deliveryId);
|
|
899
|
+
persist(agent);
|
|
900
|
+
if (agent.disabledReason) {
|
|
901
|
+
reconcileAbsentCanonical(agent, record);
|
|
902
|
+
telemetry?.delivery(agentId, messageId, "deferred");
|
|
903
|
+
return { status: "deferred", deliveryId, reason: agent.disabledReason };
|
|
904
|
+
}
|
|
905
|
+
const receipt = await (telemetry?.phase(messageId, "runtime.deliver", api_1.SpanKind.PRODUCER, () => submit(agent, record, busy)) ?? submit(agent, record, busy));
|
|
906
|
+
telemetry?.delivery(agentId, messageId, receipt.status);
|
|
907
|
+
reconcileAbsentCanonical(agent, agent.records.get(deliveryId) ?? record);
|
|
908
|
+
return receipt;
|
|
909
|
+
},
|
|
910
|
+
async stop(agentId, reason) {
|
|
911
|
+
const agent = managed.get(agentId);
|
|
912
|
+
if (!agent)
|
|
913
|
+
return;
|
|
914
|
+
agent.stopped = true;
|
|
915
|
+
if (agent.poller)
|
|
916
|
+
clearInterval(agent.poller);
|
|
917
|
+
if (agent.retryTimer)
|
|
918
|
+
clearTimeout(agent.retryTimer);
|
|
919
|
+
if (agent.stabilityTimer)
|
|
920
|
+
clearTimeout(agent.stabilityTimer);
|
|
921
|
+
if (agent.busy)
|
|
922
|
+
await agent.session?.cancel(reason);
|
|
923
|
+
await agent.session?.close(reason);
|
|
924
|
+
managed.delete(agentId);
|
|
925
|
+
emit({ type: "agent-status", agentId, status: "inactive" });
|
|
926
|
+
},
|
|
927
|
+
async shutdown(reason) { await Promise.allSettled([...managed.keys()].map((id) => this.stop(id, reason))); },
|
|
928
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
929
|
+
};
|
|
930
|
+
}
|