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.
Files changed (144) hide show
  1. package/CONTRIBUTING.md +48 -0
  2. package/LICENSE +201 -0
  3. package/README.md +122 -0
  4. package/SECURITY.md +14 -0
  5. package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
  6. package/dist/agent/agent-cli-capabilities.cjs +43 -0
  7. package/dist/agent/agent-cli-capabilities.mjs +35 -0
  8. package/dist/agent/agent-state-store.cjs +961 -0
  9. package/dist/agent/agent-state-store.mjs +923 -0
  10. package/dist/agent/agent-transport.cjs +201 -0
  11. package/dist/agent/agent-transport.mjs +194 -0
  12. package/dist/agent/config-cli-contract.cjs +19 -0
  13. package/dist/agent/config-cli-contract.mjs +16 -0
  14. package/dist/agent/context-prompt.cjs +159 -0
  15. package/dist/agent/context-prompt.mjs +155 -0
  16. package/dist/agent/freshness-gate.mjs +24 -0
  17. package/dist/agent/history-projection.cjs +72 -0
  18. package/dist/agent/history-projection.mjs +65 -0
  19. package/dist/agent/host-reminder-orchestrator.cjs +289 -0
  20. package/dist/agent/host-reminder-orchestrator.mjs +252 -0
  21. package/dist/agent/inbox-projection.cjs +92 -0
  22. package/dist/agent/inbox-projection.mjs +85 -0
  23. package/dist/agent/interaction-state-machine.cjs +710 -0
  24. package/dist/agent/interaction-state-machine.mjs +705 -0
  25. package/dist/agent/reminder-routes.cjs +192 -0
  26. package/dist/agent/reminder-routes.mjs +156 -0
  27. package/dist/agent/reminder-store.cjs +220 -0
  28. package/dist/agent/reminder-store.mjs +205 -0
  29. package/dist/agent/transport-business-context.cjs +486 -0
  30. package/dist/agent/transport-business-context.mjs +446 -0
  31. package/dist/agent/transport-shell.cjs +45 -0
  32. package/dist/agent/transport-shell.mjs +39 -0
  33. package/dist/app/agent-cli.mjs +596 -0
  34. package/dist/app/agent-config.mjs +633 -0
  35. package/dist/app/agent-lark-cli-workspace.cjs +95 -0
  36. package/dist/app/agent-lark-cli-workspace.mjs +85 -0
  37. package/dist/app/agent-readiness.cjs +42 -0
  38. package/dist/app/agent-readiness.mjs +38 -0
  39. package/dist/app/binary-entry.mjs +86 -0
  40. package/dist/app/cli.mjs +176 -0
  41. package/dist/app/dashboard.mjs +205 -0
  42. package/dist/app/internal-command.cjs +57 -0
  43. package/dist/app/internal-command.mjs +48 -0
  44. package/dist/app/lark-channel-secret.mjs +22 -0
  45. package/dist/app/lark-cli.mjs +887 -0
  46. package/dist/app/lark.mjs +45 -0
  47. package/dist/app/local-control.mjs +728 -0
  48. package/dist/app/official-lark-cli.cjs +138 -0
  49. package/dist/app/official-lark-cli.mjs +128 -0
  50. package/dist/app/pi-auth-cli.mjs +67 -0
  51. package/dist/app/run.mjs +364 -0
  52. package/dist/app/runtime-agent-config.mjs +416 -0
  53. package/dist/app/runtime-model-directory.mjs +78 -0
  54. package/dist/app/runtime-process.mjs +113 -0
  55. package/dist/app/session-cli.mjs +83 -0
  56. package/dist/app/setup-dashboard.mjs +26 -0
  57. package/dist/app/setup.mjs +166 -0
  58. package/dist/app/telemetry.mjs +83 -0
  59. package/dist/dashboard/dashboard-assets.mjs +21 -0
  60. package/dist/dashboard/dashboard-avatar.mjs +128 -0
  61. package/dist/dashboard/dashboard-config-controller.mjs +525 -0
  62. package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
  63. package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
  64. package/dist/dashboard/dashboard-template.mjs +29 -0
  65. package/dist/dashboard/dashboard-view-model.mjs +556 -0
  66. package/dist/dashboard/dashboard-workspace.mjs +104 -0
  67. package/dist/dashboard/web/assets/dashboard.css +3 -0
  68. package/dist/dashboard/web/assets/dashboard.js +49 -0
  69. package/dist/feishu/channel-transport.cjs +151 -0
  70. package/dist/feishu/channel-transport.mjs +148 -0
  71. package/dist/feishu/document-comment.cjs +120 -0
  72. package/dist/feishu/document-comment.mjs +109 -0
  73. package/dist/feishu/host-business-state.cjs +477 -0
  74. package/dist/feishu/host-business-state.mjs +462 -0
  75. package/dist/feishu/host-channel-business.cjs +161 -0
  76. package/dist/feishu/host-channel-business.mjs +157 -0
  77. package/dist/feishu/host-processing-eye.cjs +185 -0
  78. package/dist/feishu/host-processing-eye.mjs +181 -0
  79. package/dist/feishu/host-shell.cjs +1425 -0
  80. package/dist/feishu/host-shell.mjs +1385 -0
  81. package/dist/feishu/im-freshness-adapter.mjs +89 -0
  82. package/dist/feishu/interaction-orchestrator.cjs +279 -0
  83. package/dist/feishu/interaction-orchestrator.mjs +242 -0
  84. package/dist/feishu/lark-passthrough.mjs +70 -0
  85. package/dist/feishu/message-policy.cjs +111 -0
  86. package/dist/feishu/message-policy.mjs +98 -0
  87. package/dist/feishu/outbound-transport.cjs +221 -0
  88. package/dist/feishu/outbound-transport.mjs +215 -0
  89. package/dist/platform/build-info.cjs +25 -0
  90. package/dist/platform/build-info.mjs +18 -0
  91. package/dist/platform/callback-capability.cjs +301 -0
  92. package/dist/platform/callback-capability.mjs +252 -0
  93. package/dist/platform/check-bun-version.cjs +12 -0
  94. package/dist/platform/config.cjs +751 -0
  95. package/dist/platform/config.mjs +694 -0
  96. package/dist/platform/process-inspect.cjs +89 -0
  97. package/dist/platform/process-state.cjs +237 -0
  98. package/dist/platform/process-state.mjs +223 -0
  99. package/dist/platform/release-artifacts.mjs +61 -0
  100. package/dist/platform/root-layout.cjs +111 -0
  101. package/dist/platform/root-layout.mjs +72 -0
  102. package/dist/platform/telemetry-config.cjs +64 -0
  103. package/dist/platform/telemetry-config.mjs +56 -0
  104. package/dist/platform/telemetry-spool.cjs +661 -0
  105. package/dist/platform/telemetry-spool.mjs +653 -0
  106. package/dist/platform/telemetry-tracing.cjs +602 -0
  107. package/dist/platform/telemetry-tracing.mjs +596 -0
  108. package/dist/platform/telemetry-uploader.cjs +150 -0
  109. package/dist/platform/telemetry-uploader.mjs +146 -0
  110. package/dist/platform/workspace-service.cjs +469 -0
  111. package/dist/platform/workspace-service.mjs +430 -0
  112. package/dist/runtime/builtin-pi-assets.mjs +33 -0
  113. package/dist/runtime/claude-model-catalog.cjs +102 -0
  114. package/dist/runtime/claude-model-catalog.mjs +99 -0
  115. package/dist/runtime/codex-model-catalog.cjs +108 -0
  116. package/dist/runtime/codex-model-catalog.mjs +105 -0
  117. package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
  118. package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
  119. package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
  120. package/dist/runtime/pi-model-catalog.cjs +116 -0
  121. package/dist/runtime/pi-model-catalog.mjs +75 -0
  122. package/dist/runtime/pi-official-auth.mjs +480 -0
  123. package/dist/runtime/pi-provider-config.cjs +252 -0
  124. package/dist/runtime/pi-provider-config.mjs +239 -0
  125. package/dist/runtime/pi-rpc-client.cjs +255 -0
  126. package/dist/runtime/pi-rpc-client.mjs +251 -0
  127. package/dist/runtime/pi-subagent-injection.mjs +129 -0
  128. package/dist/runtime/pi-subagents.bundle.js +8990 -0
  129. package/dist/runtime/runtime-adapters.mjs +1019 -0
  130. package/dist/runtime/runtime-contracts.cjs +2 -0
  131. package/dist/runtime/runtime-contracts.mjs +1 -0
  132. package/dist/runtime/runtime-host.cjs +930 -0
  133. package/dist/runtime/runtime-host.mjs +922 -0
  134. package/dist/runtime/runtime-model-catalog.cjs +43 -0
  135. package/dist/runtime/runtime-model-catalog.mjs +40 -0
  136. package/dist/runtime/runtime-readiness.cjs +177 -0
  137. package/dist/runtime/runtime-readiness.mjs +133 -0
  138. package/dist/setup/bot-register.mjs +655 -0
  139. package/dist/setup/grant-scopes.mjs +133 -0
  140. package/dist/setup/run-credential-preflight.mjs +89 -0
  141. package/dist/setup/setup-agent-choice.mjs +172 -0
  142. package/dist/setup/setup-bind.mjs +337 -0
  143. package/dist/setup/setup-binding.mjs +52 -0
  144. package/package.json +112 -0
@@ -0,0 +1,89 @@
1
+ function integerMillis(value) {
2
+ if (typeof value !== "string" || !/^[0-9]+$/.test(value))
3
+ return null;
4
+ try {
5
+ return BigInt(value);
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ export function messageRevisionTime(message) {
12
+ const created = integerMillis(message.create_time);
13
+ const updated = message.update_time === undefined || message.update_time === "" ? created : integerMillis(message.update_time);
14
+ if (created === null || updated === null)
15
+ throw new Error(`malformed Feishu IM revision for ${message.message_id || "unknown message"}`);
16
+ return created > updated ? created : updated;
17
+ }
18
+ export function feishuImTarget(value) {
19
+ const thread = /^thread:([^:]+):(.+)$/.exec(value);
20
+ if (thread)
21
+ return { provider: "feishu.im", resourceKind: "thread", resourceId: `${thread[1]}/${thread[2]}` };
22
+ const chat = /^chat:(.+)$/.exec(value);
23
+ if (chat)
24
+ return { provider: "feishu.im", resourceKind: "chat", resourceId: chat[1] };
25
+ throw new Error(`unsupported Feishu IM freshness target: ${value}`);
26
+ }
27
+ export function serializeFeishuImTarget(target) {
28
+ return `${target.provider}/${target.resourceKind}/${target.resourceId}`;
29
+ }
30
+ function normalizedMessages(snapshot) {
31
+ if (!snapshot || !Array.isArray(snapshot.messages))
32
+ throw new Error("malformed Feishu IM history payload: messages must be an array");
33
+ return snapshot.messages.map((message) => {
34
+ if (!message || typeof message !== "object" || typeof message.message_id !== "string" || !message.message_id) {
35
+ throw new Error("malformed Feishu IM history payload: message_id is required");
36
+ }
37
+ return { ...message, revision: messageRevisionTime(message) };
38
+ });
39
+ }
40
+ export const feishuImFreshnessAdapter = {
41
+ cursor(snapshot) {
42
+ const messages = normalizedMessages(snapshot);
43
+ if (!messages.length)
44
+ return null;
45
+ const revision = messages.reduce((maximum, message) => message.revision > maximum ? message.revision : maximum, 0n);
46
+ return {
47
+ schema: 1,
48
+ revisionTime: revision.toString(),
49
+ messageIds: [...new Set(messages.filter((message) => message.revision === revision).map((message) => message.message_id))].sort(),
50
+ };
51
+ },
52
+ compare(seen, current) {
53
+ if (!current)
54
+ return seen ? "gap" : "fresh";
55
+ if (!seen)
56
+ return "conflict";
57
+ const seenTime = integerMillis(seen.revisionTime);
58
+ const currentTime = integerMillis(current.revisionTime);
59
+ if (seen.schema !== 1 || seenTime === null || currentTime === null)
60
+ return "gap";
61
+ if (currentTime < seenTime)
62
+ return "gap";
63
+ if (currentTime > seenTime)
64
+ return "conflict";
65
+ return current.messageIds.every((id) => seen.messageIds.includes(id)) ? "fresh" : "conflict";
66
+ },
67
+ unseen(seen, snapshot) {
68
+ const messages = normalizedMessages(snapshot);
69
+ if (!seen)
70
+ return messages.map(({ revision: _revision, ...message }) => message);
71
+ const seenTime = integerMillis(seen.revisionTime);
72
+ if (seenTime === null)
73
+ return messages.map(({ revision: _revision, ...message }) => message);
74
+ return messages.filter((message) => message.revision > seenTime
75
+ || (message.revision === seenTime && !seen.messageIds.includes(message.message_id)))
76
+ .map(({ revision: _revision, ...message }) => message);
77
+ },
78
+ };
79
+ export function mergeFeishuImCursor(left, right) {
80
+ if (!left)
81
+ return { ...right, messageIds: [...right.messageIds] };
82
+ const leftTime = BigInt(left.revisionTime);
83
+ const rightTime = BigInt(right.revisionTime);
84
+ if (rightTime < leftTime)
85
+ return left;
86
+ if (rightTime > leftTime)
87
+ return { ...right, messageIds: [...right.messageIds] };
88
+ return { schema: 1, revisionTime: left.revisionTime, messageIds: [...new Set([...left.messageIds, ...right.messageIds])].sort() };
89
+ }
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HostInteractionOrchestrator = void 0;
37
+ const node_crypto_1 = require("node:crypto");
38
+ const fs = __importStar(require("node:fs"));
39
+ const interaction_state_machine_js_1 = require("../agent/interaction-state-machine.cjs");
40
+ const ReminderStore = __importStar(require("../agent/reminder-store.cjs"));
41
+ function isRecord(value) {
42
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
43
+ }
44
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
45
+ function rawEventId(raw) {
46
+ if (!isRecord(raw))
47
+ return null;
48
+ const header = isRecord(raw.header) ? raw.header : null;
49
+ const event = isRecord(raw.event) ? raw.event : null;
50
+ for (const value of [raw.event_id, header?.event_id, event?.event_id])
51
+ if (typeof value === "string" && value)
52
+ return value;
53
+ return null;
54
+ }
55
+ function interactionRef(event) {
56
+ if (!isRecord(event.action.value) || typeof event.action.value.interaction_ref !== "string")
57
+ throw new Error("card action is not a Larkin interaction");
58
+ return event.action.value.interaction_ref;
59
+ }
60
+ function interactionVersion(event) {
61
+ if (!isRecord(event.action.value) || !Number.isSafeInteger(event.action.value.interaction_version))
62
+ throw new Error("card action interaction_version is missing");
63
+ return Number(event.action.value.interaction_version);
64
+ }
65
+ class HostInteractionOrchestrator {
66
+ options;
67
+ log;
68
+ now;
69
+ watchers = new Map();
70
+ syncAgents = new Map();
71
+ flushInFlight = new Map();
72
+ syncTimer = null;
73
+ syncActive = false;
74
+ constructor(options) {
75
+ this.options = options;
76
+ this.log = options.log ?? (() => { });
77
+ this.now = options.now ?? Date.now;
78
+ }
79
+ machine(agent) {
80
+ return new interaction_state_machine_js_1.InteractionStateMachine({ stateStore: this.options.stateStore(agent), agentId: agent.agentId, now: this.now });
81
+ }
82
+ callbackId(event) {
83
+ const eventId = rawEventId(event.raw);
84
+ if (!eventId)
85
+ throw new Error("card callback event_id is missing; enable includeRawEvent and retry");
86
+ return `card:${eventId}`;
87
+ }
88
+ runReminderEffect(agent, run, effect, deadline) {
89
+ if (deadline - this.now() < 500)
90
+ return { status: "timed_out", summary: "reminder.schedule skipped because the Reflex deadline was exhausted", data: { category: "reflex_deadline" } };
91
+ try {
92
+ const file = this.options.stateStore(agent).paths.reminders;
93
+ const reminderId = `int_${(0, node_crypto_1.createHash)("sha256").update(run.run_id).digest("hex").slice(0, 24)}`;
94
+ const current = this.now();
95
+ const fireAt = effect.args.fire_at ? Date.parse(effect.args.fire_at) : current + Number(effect.args.delay_seconds) * 1_000;
96
+ const reminder = ReminderStore.mutate(file, (store) => {
97
+ const existing = store.reminders.find((candidate) => candidate.reminderId === reminderId);
98
+ if (existing)
99
+ return existing;
100
+ const created = {
101
+ reminderId,
102
+ ownerAgentId: agent.agentId,
103
+ title: effect.args.title,
104
+ fireAt: ReminderStore.nowIso(fireAt),
105
+ firedAt: null,
106
+ createdAt: ReminderStore.nowIso(current),
107
+ status: "scheduled",
108
+ msgRef: run.message_id,
109
+ msgPermalink: null,
110
+ repeat: null,
111
+ tz: null,
112
+ channel: run.chat_id,
113
+ payload: { source: "interaction", run_id: run.run_id },
114
+ version: 1,
115
+ events: [],
116
+ };
117
+ ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current);
118
+ store.reminders.push(created);
119
+ return created;
120
+ }, Math.max(1, Math.min(350, deadline - this.now() - 100)));
121
+ return { status: "succeeded", summary: "reminder.schedule completed", data: { reminder_id: reminder.reminderId, fire_at: reminder.fireAt } };
122
+ }
123
+ catch (error) {
124
+ const timedOut = this.now() >= deadline || /锁等待超时/.test(errorMessage(error));
125
+ return timedOut
126
+ ? { status: "timed_out", summary: "reminder.schedule did not complete within the Reflex deadline", data: { category: "reflex_deadline" } }
127
+ : { status: "uncertain", summary: "reminder.schedule could not confirm its durable result; inspect before retrying", data: { category: "reminder_schedule_uncertain" } };
128
+ }
129
+ }
130
+ async handleCardAction(agent, event) {
131
+ const deadline = this.now() + 2_700;
132
+ const machine = this.machine(agent);
133
+ try {
134
+ const claimed = machine.claim({
135
+ interaction_ref: interactionRef(event),
136
+ expected_version: interactionVersion(event),
137
+ callback_id: this.callbackId(event),
138
+ operator_open_id: event.operator.openId,
139
+ chat_id: event.chatId,
140
+ message_id: event.messageId,
141
+ });
142
+ let run = claimed.run;
143
+ if (run.reflex.status === "pending") {
144
+ const effect = claimed.definition.actions[run.action_id].reflex.effect;
145
+ const result = effect
146
+ ? this.runReminderEffect(agent, run, effect, deadline)
147
+ : { status: "succeeded", summary: "default Reflex accepted the action", data: {} };
148
+ run = machine.recordReflex(run.run_id, result);
149
+ }
150
+ void this.flushPending(agent).catch((error) => this.log(`interaction 异步投递失败 agent=${agent.name}: ${errorMessage(error)}`));
151
+ const latest = machine.get({ run_id: run.run_id });
152
+ const effectFailed = ["failed", "uncertain", "timed_out"].includes(run.reflex.status);
153
+ return {
154
+ toast: {
155
+ type: effectFailed ? "warning" : "info",
156
+ content: effectFailed ? "快速操作结果尚未确认,Agent 已接管;当前不代表业务完成。" : "已受理,Agent 正在处理,完成后会更新卡片。",
157
+ },
158
+ card: { type: "raw", data: latest.card },
159
+ };
160
+ }
161
+ catch (error) {
162
+ this.log(`interaction callback 拒绝 agent=${agent.name}: ${errorMessage(error)}`);
163
+ const reason = errorMessage(error);
164
+ const safeReason = /expired|not allowed|already has an active run|transition limit|reference is invalid|event_id is missing|version is stale|interaction_version is missing/.test(reason)
165
+ ? reason : "interaction service is temporarily unavailable";
166
+ return { toast: { type: "error", content: `无法受理:${safeReason}` } };
167
+ }
168
+ }
169
+ async flushPending(agent) {
170
+ const existing = this.flushInFlight.get(agent.agentId);
171
+ if (existing)
172
+ return existing;
173
+ const attempt = this.performFlush(agent);
174
+ this.flushInFlight.set(agent.agentId, attempt);
175
+ try {
176
+ await attempt;
177
+ }
178
+ finally {
179
+ if (this.flushInFlight.get(agent.agentId) === attempt)
180
+ this.flushInFlight.delete(agent.agentId);
181
+ }
182
+ }
183
+ async performFlush(agent) {
184
+ const machine = this.machine(agent);
185
+ const maintenance = machine.pendingMaintenance();
186
+ if (maintenance.interrupted_reflex)
187
+ machine.recoverInterruptedReflexes();
188
+ if (maintenance.timed_out_run)
189
+ machine.expireTimedOutRuns();
190
+ for (let pass = 0; pass < 4; pass += 1) {
191
+ const pending = machine.pendingOutbox();
192
+ if (!pending.length)
193
+ break;
194
+ let hadFailure = false;
195
+ for (const item of pending) {
196
+ try {
197
+ if (item.kind === "agent_wake") {
198
+ const envelope = item.payload;
199
+ const preparation = this.options.stateStore(agent).prepareInboxDelivery(envelope);
200
+ if (preparation !== "consumed") {
201
+ const receipt = await this.options.deliveryTarget.deliver(agent.agentId, envelope);
202
+ if (!receipt || !["accepted", "duplicate", "deferred"].includes(String(receipt.status))) {
203
+ throw new Error(`Runtime delivery returned ${String(receipt?.status || "unknown")}: ${String(receipt?.reason || "no reason")}`);
204
+ }
205
+ this.options.afterRuntimeDelivery?.();
206
+ }
207
+ machine.markOutbox(item.outbox_id, { delivered: true });
208
+ }
209
+ else {
210
+ const channel = this.options.channelFor(agent);
211
+ if (!channel)
212
+ throw new Error("card channel is not connected");
213
+ const projection = machine.prepareProjection(item.outbox_id);
214
+ if (!projection)
215
+ continue;
216
+ try {
217
+ await channel.updateCard(projection.message_id, projection.card);
218
+ machine.completeProjection(item.outbox_id, projection.desired_version, { delivered: true });
219
+ }
220
+ catch (error) {
221
+ machine.completeProjection(item.outbox_id, projection.desired_version, { delivered: false, error: errorMessage(error) });
222
+ throw error;
223
+ }
224
+ }
225
+ }
226
+ catch (error) {
227
+ hadFailure = true;
228
+ if (item.kind === "agent_wake")
229
+ machine.markOutbox(item.outbox_id, { delivered: false, error: errorMessage(error) });
230
+ this.log(`interaction outbox 保留重试 agent=${agent.name} kind=${item.kind}: ${errorMessage(error)}`);
231
+ }
232
+ }
233
+ if (hadFailure)
234
+ break;
235
+ }
236
+ }
237
+ startSync() {
238
+ if (this.syncTimer)
239
+ return;
240
+ this.syncActive = true;
241
+ for (const agent of this.options.agents)
242
+ this.syncAgent(agent);
243
+ this.syncTimer = (this.options.setInterval ?? setInterval)(() => {
244
+ for (const agent of this.syncAgents.values())
245
+ void this.flushPending(agent);
246
+ }, 30_000);
247
+ this.syncTimer.unref?.();
248
+ }
249
+ syncAgent(agent) {
250
+ this.syncAgents.set(agent.agentId, agent);
251
+ if (!this.syncActive)
252
+ return;
253
+ if (agent.stateDir && !this.watchers.has(agent.agentId)) {
254
+ try {
255
+ const watcher = (this.options.watch ?? fs.watch)(agent.stateDir, (_event, filename) => {
256
+ const current = this.syncAgents.get(agent.agentId);
257
+ if (filename === "interactions.json" && current)
258
+ void this.flushPending(current);
259
+ });
260
+ this.watchers.set(agent.agentId, watcher);
261
+ }
262
+ catch (error) {
263
+ this.log(`interaction watch 失败 agent=${agent.name}: ${errorMessage(error)}`);
264
+ }
265
+ }
266
+ void this.flushPending(agent);
267
+ }
268
+ stopSync() {
269
+ if (this.syncTimer)
270
+ clearInterval(this.syncTimer);
271
+ this.syncTimer = null;
272
+ this.syncActive = false;
273
+ for (const watcher of this.watchers.values())
274
+ watcher.close();
275
+ this.watchers.clear();
276
+ this.syncAgents.clear();
277
+ }
278
+ }
279
+ exports.HostInteractionOrchestrator = HostInteractionOrchestrator;
@@ -0,0 +1,242 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import { InteractionStateMachine } from "../agent/interaction-state-machine.mjs";
4
+ import * as ReminderStore from "../agent/reminder-store.mjs";
5
+ function isRecord(value) {
6
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
9
+ function rawEventId(raw) {
10
+ if (!isRecord(raw))
11
+ return null;
12
+ const header = isRecord(raw.header) ? raw.header : null;
13
+ const event = isRecord(raw.event) ? raw.event : null;
14
+ for (const value of [raw.event_id, header?.event_id, event?.event_id])
15
+ if (typeof value === "string" && value)
16
+ return value;
17
+ return null;
18
+ }
19
+ function interactionRef(event) {
20
+ if (!isRecord(event.action.value) || typeof event.action.value.interaction_ref !== "string")
21
+ throw new Error("card action is not a Larkin interaction");
22
+ return event.action.value.interaction_ref;
23
+ }
24
+ function interactionVersion(event) {
25
+ if (!isRecord(event.action.value) || !Number.isSafeInteger(event.action.value.interaction_version))
26
+ throw new Error("card action interaction_version is missing");
27
+ return Number(event.action.value.interaction_version);
28
+ }
29
+ export class HostInteractionOrchestrator {
30
+ options;
31
+ log;
32
+ now;
33
+ watchers = new Map();
34
+ syncAgents = new Map();
35
+ flushInFlight = new Map();
36
+ syncTimer = null;
37
+ syncActive = false;
38
+ constructor(options) {
39
+ this.options = options;
40
+ this.log = options.log ?? (() => { });
41
+ this.now = options.now ?? Date.now;
42
+ }
43
+ machine(agent) {
44
+ return new InteractionStateMachine({ stateStore: this.options.stateStore(agent), agentId: agent.agentId, now: this.now });
45
+ }
46
+ callbackId(event) {
47
+ const eventId = rawEventId(event.raw);
48
+ if (!eventId)
49
+ throw new Error("card callback event_id is missing; enable includeRawEvent and retry");
50
+ return `card:${eventId}`;
51
+ }
52
+ runReminderEffect(agent, run, effect, deadline) {
53
+ if (deadline - this.now() < 500)
54
+ return { status: "timed_out", summary: "reminder.schedule skipped because the Reflex deadline was exhausted", data: { category: "reflex_deadline" } };
55
+ try {
56
+ const file = this.options.stateStore(agent).paths.reminders;
57
+ const reminderId = `int_${createHash("sha256").update(run.run_id).digest("hex").slice(0, 24)}`;
58
+ const current = this.now();
59
+ const fireAt = effect.args.fire_at ? Date.parse(effect.args.fire_at) : current + Number(effect.args.delay_seconds) * 1_000;
60
+ const reminder = ReminderStore.mutate(file, (store) => {
61
+ const existing = store.reminders.find((candidate) => candidate.reminderId === reminderId);
62
+ if (existing)
63
+ return existing;
64
+ const created = {
65
+ reminderId,
66
+ ownerAgentId: agent.agentId,
67
+ title: effect.args.title,
68
+ fireAt: ReminderStore.nowIso(fireAt),
69
+ firedAt: null,
70
+ createdAt: ReminderStore.nowIso(current),
71
+ status: "scheduled",
72
+ msgRef: run.message_id,
73
+ msgPermalink: null,
74
+ repeat: null,
75
+ tz: null,
76
+ channel: run.chat_id,
77
+ payload: { source: "interaction", run_id: run.run_id },
78
+ version: 1,
79
+ events: [],
80
+ };
81
+ ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current);
82
+ store.reminders.push(created);
83
+ return created;
84
+ }, Math.max(1, Math.min(350, deadline - this.now() - 100)));
85
+ return { status: "succeeded", summary: "reminder.schedule completed", data: { reminder_id: reminder.reminderId, fire_at: reminder.fireAt } };
86
+ }
87
+ catch (error) {
88
+ const timedOut = this.now() >= deadline || /锁等待超时/.test(errorMessage(error));
89
+ return timedOut
90
+ ? { status: "timed_out", summary: "reminder.schedule did not complete within the Reflex deadline", data: { category: "reflex_deadline" } }
91
+ : { status: "uncertain", summary: "reminder.schedule could not confirm its durable result; inspect before retrying", data: { category: "reminder_schedule_uncertain" } };
92
+ }
93
+ }
94
+ async handleCardAction(agent, event) {
95
+ const deadline = this.now() + 2_700;
96
+ const machine = this.machine(agent);
97
+ try {
98
+ const claimed = machine.claim({
99
+ interaction_ref: interactionRef(event),
100
+ expected_version: interactionVersion(event),
101
+ callback_id: this.callbackId(event),
102
+ operator_open_id: event.operator.openId,
103
+ chat_id: event.chatId,
104
+ message_id: event.messageId,
105
+ });
106
+ let run = claimed.run;
107
+ if (run.reflex.status === "pending") {
108
+ const effect = claimed.definition.actions[run.action_id].reflex.effect;
109
+ const result = effect
110
+ ? this.runReminderEffect(agent, run, effect, deadline)
111
+ : { status: "succeeded", summary: "default Reflex accepted the action", data: {} };
112
+ run = machine.recordReflex(run.run_id, result);
113
+ }
114
+ void this.flushPending(agent).catch((error) => this.log(`interaction 异步投递失败 agent=${agent.name}: ${errorMessage(error)}`));
115
+ const latest = machine.get({ run_id: run.run_id });
116
+ const effectFailed = ["failed", "uncertain", "timed_out"].includes(run.reflex.status);
117
+ return {
118
+ toast: {
119
+ type: effectFailed ? "warning" : "info",
120
+ content: effectFailed ? "快速操作结果尚未确认,Agent 已接管;当前不代表业务完成。" : "已受理,Agent 正在处理,完成后会更新卡片。",
121
+ },
122
+ card: { type: "raw", data: latest.card },
123
+ };
124
+ }
125
+ catch (error) {
126
+ this.log(`interaction callback 拒绝 agent=${agent.name}: ${errorMessage(error)}`);
127
+ const reason = errorMessage(error);
128
+ const safeReason = /expired|not allowed|already has an active run|transition limit|reference is invalid|event_id is missing|version is stale|interaction_version is missing/.test(reason)
129
+ ? reason : "interaction service is temporarily unavailable";
130
+ return { toast: { type: "error", content: `无法受理:${safeReason}` } };
131
+ }
132
+ }
133
+ async flushPending(agent) {
134
+ const existing = this.flushInFlight.get(agent.agentId);
135
+ if (existing)
136
+ return existing;
137
+ const attempt = this.performFlush(agent);
138
+ this.flushInFlight.set(agent.agentId, attempt);
139
+ try {
140
+ await attempt;
141
+ }
142
+ finally {
143
+ if (this.flushInFlight.get(agent.agentId) === attempt)
144
+ this.flushInFlight.delete(agent.agentId);
145
+ }
146
+ }
147
+ async performFlush(agent) {
148
+ const machine = this.machine(agent);
149
+ const maintenance = machine.pendingMaintenance();
150
+ if (maintenance.interrupted_reflex)
151
+ machine.recoverInterruptedReflexes();
152
+ if (maintenance.timed_out_run)
153
+ machine.expireTimedOutRuns();
154
+ for (let pass = 0; pass < 4; pass += 1) {
155
+ const pending = machine.pendingOutbox();
156
+ if (!pending.length)
157
+ break;
158
+ let hadFailure = false;
159
+ for (const item of pending) {
160
+ try {
161
+ if (item.kind === "agent_wake") {
162
+ const envelope = item.payload;
163
+ const preparation = this.options.stateStore(agent).prepareInboxDelivery(envelope);
164
+ if (preparation !== "consumed") {
165
+ const receipt = await this.options.deliveryTarget.deliver(agent.agentId, envelope);
166
+ if (!receipt || !["accepted", "duplicate", "deferred"].includes(String(receipt.status))) {
167
+ throw new Error(`Runtime delivery returned ${String(receipt?.status || "unknown")}: ${String(receipt?.reason || "no reason")}`);
168
+ }
169
+ this.options.afterRuntimeDelivery?.();
170
+ }
171
+ machine.markOutbox(item.outbox_id, { delivered: true });
172
+ }
173
+ else {
174
+ const channel = this.options.channelFor(agent);
175
+ if (!channel)
176
+ throw new Error("card channel is not connected");
177
+ const projection = machine.prepareProjection(item.outbox_id);
178
+ if (!projection)
179
+ continue;
180
+ try {
181
+ await channel.updateCard(projection.message_id, projection.card);
182
+ machine.completeProjection(item.outbox_id, projection.desired_version, { delivered: true });
183
+ }
184
+ catch (error) {
185
+ machine.completeProjection(item.outbox_id, projection.desired_version, { delivered: false, error: errorMessage(error) });
186
+ throw error;
187
+ }
188
+ }
189
+ }
190
+ catch (error) {
191
+ hadFailure = true;
192
+ if (item.kind === "agent_wake")
193
+ machine.markOutbox(item.outbox_id, { delivered: false, error: errorMessage(error) });
194
+ this.log(`interaction outbox 保留重试 agent=${agent.name} kind=${item.kind}: ${errorMessage(error)}`);
195
+ }
196
+ }
197
+ if (hadFailure)
198
+ break;
199
+ }
200
+ }
201
+ startSync() {
202
+ if (this.syncTimer)
203
+ return;
204
+ this.syncActive = true;
205
+ for (const agent of this.options.agents)
206
+ this.syncAgent(agent);
207
+ this.syncTimer = (this.options.setInterval ?? setInterval)(() => {
208
+ for (const agent of this.syncAgents.values())
209
+ void this.flushPending(agent);
210
+ }, 30_000);
211
+ this.syncTimer.unref?.();
212
+ }
213
+ syncAgent(agent) {
214
+ this.syncAgents.set(agent.agentId, agent);
215
+ if (!this.syncActive)
216
+ return;
217
+ if (agent.stateDir && !this.watchers.has(agent.agentId)) {
218
+ try {
219
+ const watcher = (this.options.watch ?? fs.watch)(agent.stateDir, (_event, filename) => {
220
+ const current = this.syncAgents.get(agent.agentId);
221
+ if (filename === "interactions.json" && current)
222
+ void this.flushPending(current);
223
+ });
224
+ this.watchers.set(agent.agentId, watcher);
225
+ }
226
+ catch (error) {
227
+ this.log(`interaction watch 失败 agent=${agent.name}: ${errorMessage(error)}`);
228
+ }
229
+ }
230
+ void this.flushPending(agent);
231
+ }
232
+ stopSync() {
233
+ if (this.syncTimer)
234
+ clearInterval(this.syncTimer);
235
+ this.syncTimer = null;
236
+ this.syncActive = false;
237
+ for (const watcher of this.watchers.values())
238
+ watcher.close();
239
+ this.watchers.clear();
240
+ this.syncAgents.clear();
241
+ }
242
+ }
@@ -0,0 +1,70 @@
1
+ // larkin 顶级 lark-cli 命令组直通决策(larkin im … == lark-cli im …):封装转发不改写语义,
2
+ // 唯一硬边界是身份/凭证(Owner 定,2026-07-17,见 coding-context larkin.cli-prompt-alignment plan)。
3
+ // 能力边界交给飞书权限体系(bot scope、群成员身份、文档协作者),壳不做命令级裁剪;
4
+ // 不可逆操作靠版本化 Runtime standing prompt 的行为约定治理。
5
+ import { resolveRuntimeAuthority } from "../platform/config.mjs";
6
+ export const PASSTHROUGH_USAGE = "用法:larkin <lark-cli 命令组…> [--agent <App ID>],如 larkin im +chat-list --json(语法与 lark-cli 一致)";
7
+ const IDENTITY = "身份边界";
8
+ export function assessPassthrough(argv, runtimeEnv = {}) {
9
+ const rest = [...argv];
10
+ let runtimeAgentId;
11
+ try {
12
+ runtimeAgentId = resolveRuntimeAuthority(runtimeEnv);
13
+ }
14
+ catch (error) {
15
+ return { ok: false, reason: `${IDENTITY}:${error instanceof Error ? error.message : String(error)}` };
16
+ }
17
+ let explicitAgent = null;
18
+ const agentFlags = rest.reduce((indexes, argument, index) => {
19
+ if (argument === "--agent")
20
+ indexes.push(index);
21
+ return indexes;
22
+ }, []);
23
+ if (agentFlags.length > 1) {
24
+ return { ok: false, reason: `--agent 只能指定一次。${PASSTHROUGH_USAGE}` };
25
+ }
26
+ const agentFlag = agentFlags[0] ?? -1;
27
+ if (agentFlag >= 0) {
28
+ if (runtimeAgentId) {
29
+ return { ok: false, reason: `${IDENTITY}:当前 Agent 的机器人身份已由 Runtime authority marker 锁定,不允许使用 --agent 选择身份。` };
30
+ }
31
+ if (!rest[agentFlag + 1])
32
+ return { ok: false, reason: `--agent 需要 App ID。${PASSTHROUGH_USAGE}` };
33
+ explicitAgent = rest[agentFlag + 1];
34
+ rest.splice(agentFlag, 2);
35
+ }
36
+ if (!rest.length)
37
+ return { ok: false, reason: `缺少 lark-cli 参数。${PASSTHROUGH_USAGE}` };
38
+ for (let index = 0; index < rest.length; index += 1) {
39
+ const argument = rest[index];
40
+ if ((argument === "--as" && rest[index + 1] === "user") || argument === "--as=user") {
41
+ return { ok: false, reason: `${IDENTITY}:larkin agent 只能以自己的机器人身份调用 lark-cli,不允许 --as user。` };
42
+ }
43
+ if (argument === "--profile" || argument.startsWith("--profile=")) {
44
+ return { ok: false, reason: `${IDENTITY}:profile 由 larkin 按当前 agent 锁定,不接受手动指定;换 agent 用 --agent <App ID>。` };
45
+ }
46
+ if (argument === "--config-dir" || argument.startsWith("--config-dir=")) {
47
+ return { ok: false, reason: `${IDENTITY}:lark-cli 配置目录由 larkin 锁定,不接受覆盖。` };
48
+ }
49
+ }
50
+ const subcommand = rest.find((argument) => !argument.startsWith("-"));
51
+ // lark-cli 的 CLI 管理族操作共享凭证库与安装本体,属身份基础设施,不是飞书能力。
52
+ if (subcommand && ["auth", "config", "profile", "update"].includes(subcommand)) {
53
+ return { ok: false, reason: `${IDENTITY}:lark-cli 凭证与配置管理(${subcommand})不走 larkin;机器人凭证由 larkin setup 负责。` };
54
+ }
55
+ // event 会用同一 bot 凭证另开长连接,可能抢走 Runtime Host 的事件流(Owner 定,2026-07-17)。
56
+ if (subcommand === "event") {
57
+ return { ok: false, reason: "运行时保护:lark-cli event 会与 Runtime Host 争抢同一机器人的事件流;实时消息请用 inbox check(canonical Inbox)。" };
58
+ }
59
+ // 纯用户身份域:机器人没有邮箱/考勤/OKR 实体,直通必失败(Owner 定,2026-07-17)。
60
+ if (subcommand && ["mail", "attendance", "okr"].includes(subcommand)) {
61
+ return { ok: false, reason: `能力边界:${subcommand} 是用户身份域,机器人身份没有对应实体,larkin 不转发。` };
62
+ }
63
+ return { ok: true, argv: applyArgvTweaks(rest), explicitAgent };
64
+ }
65
+ export const ARGV_TWEAKS = {};
66
+ export function applyArgvTweaks(argv) {
67
+ const key = argv.filter((argument) => !argument.startsWith("-")).slice(0, 2).join(" ");
68
+ const tweak = ARGV_TWEAKS[key];
69
+ return tweak ? tweak([...argv]) : argv;
70
+ }