maka-agent 0.2.1-dev.90.20261017 → 0.2.1-dev.91.20261018

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 (41) hide show
  1. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  2. package/node_modules/@maka/core/dist/runtime-event.js +3 -0
  3. package/node_modules/@maka/core/dist/session.js +2 -1
  4. package/node_modules/@maka/core/dist/turn-origin.js +14 -0
  5. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +22 -0
  6. package/node_modules/@maka/runtime/dist/plugin-agent-service.js +7 -1
  7. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -0
  8. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
  9. package/node_modules/@maka/runtime-host/dist/assistant/bot/avatar.js +89 -0
  10. package/node_modules/@maka/runtime-host/dist/assistant/bot/bridge.js +52 -0
  11. package/node_modules/@maka/runtime-host/dist/assistant/bot/capabilities.js +52 -0
  12. package/node_modules/@maka/runtime-host/dist/assistant/bot/controller.js +300 -0
  13. package/node_modules/@maka/runtime-host/dist/assistant/bot/conversation.js +116 -0
  14. package/node_modules/@maka/runtime-host/dist/assistant/bot/host.js +133 -0
  15. package/node_modules/@maka/runtime-host/dist/assistant/bot/store.js +223 -0
  16. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/assistant.js +3 -1
  17. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/controller.js +61 -4
  18. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/host.js +88 -14
  19. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/prompt.js +1 -2
  20. package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/store.js +4 -3
  21. package/node_modules/@maka/runtime-host/dist/assistant/memory-network/controller.js +185 -29
  22. package/node_modules/@maka/runtime-host/dist/assistant/memory-network/host.js +18 -11
  23. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/clarification.js +124 -0
  24. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/controller.js +23 -2
  25. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/delegation.js +29 -11
  26. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/delivery.js +11 -0
  27. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/host.js +2 -7
  28. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/prompt.js +1 -1
  29. package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/store.js +5 -3
  30. package/node_modules/@maka/runtime-host/dist/assistant/service.js +23 -0
  31. package/node_modules/@maka/runtime-host/dist/protocol/index.js +7 -1
  32. package/node_modules/@maka/runtime-host/dist/protocol/message.js +20 -1
  33. package/node_modules/@maka/runtime-host/dist/protocol/personal-assistant.js +33 -0
  34. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +3 -1
  35. package/node_modules/@maka/runtime-host/dist/server/avatar-image-source.js +66 -0
  36. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +154 -33
  37. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +3 -2
  38. package/node_modules/@maka/storage/dist/agent-run-store-contract.js +3 -1
  39. package/node_modules/@maka/storage/dist/cursor-session-adapter.js +604 -0
  40. package/node_modules/@maka/storage/dist/external-session-adapters.js +2 -0
  41. package/package.json +1 -1
@@ -173,6 +173,7 @@ export function runtimeEventEnvelopeValueDomains() {
173
173
  }
174
174
  const RUNTIME_EVENT_SHAPE = defineObjectShape()(['id', 'invocationId', 'runId', 'sessionId', 'turnId', 'ts', 'partial', 'role', 'author'], ['branch', 'origin', 'modelVisibility', 'status', 'content', 'actions', 'refs']);
175
175
  const TEXT_CONTENT_SHAPE = defineObjectShape()(['kind', 'text'], [
176
+ 'summary',
176
177
  'displayText',
177
178
  'interrupted',
178
179
  'origin',
@@ -377,6 +378,7 @@ export function decodeRuntimeEvent(value) {
377
378
  content: {
378
379
  kind: 'text',
379
380
  ...normalizeMessageContent(value.content),
381
+ ...(value.content.summary !== undefined ? { summary: value.content.summary } : {}),
380
382
  ...(value.content.origin !== undefined
381
383
  ? { origin: decodeTurnOrigin(value.content.origin) }
382
384
  : {}),
@@ -403,6 +405,7 @@ function isRuntimeEventContent(value) {
403
405
  switch (value.kind) {
404
406
  case 'text':
405
407
  if (!hasExactShape(value, TEXT_CONTENT_SHAPE) ||
408
+ (value.summary !== undefined && typeof value.summary !== 'string') ||
406
409
  (value.origin !== undefined && !isTurnOrigin(value.origin)) ||
407
410
  (value.steering !== undefined && value.steering !== true) ||
408
411
  (value.interrupted !== undefined && value.interrupted !== true) ||
@@ -393,7 +393,7 @@ const USER_MESSAGE_SHAPE = defineObjectShape()(['type', 'id', 'turnId', 'ts', 't
393
393
  'coordinationActionId',
394
394
  'origin',
395
395
  ]);
396
- const ASSISTANT_MESSAGE_SHAPE = defineObjectShape()(['type', 'id', 'turnId', 'ts', 'text', 'modelId'], ['thinking', 'contentOrder', 'providerOptions', 'interrupted']);
396
+ const ASSISTANT_MESSAGE_SHAPE = defineObjectShape()(['type', 'id', 'turnId', 'ts', 'text', 'modelId'], ['thinking', 'contentOrder', 'providerOptions', 'interrupted', 'summary']);
397
397
  const TOOL_CALL_MESSAGE_SHAPE = defineObjectShape()(['type', 'id', 'turnId', 'ts', 'toolName', 'args'], [
398
398
  'activityKind',
399
399
  'displayName',
@@ -610,6 +610,7 @@ function decodeMessage(value, decodeToolResultContent) {
610
610
  hasMessageEnvelope(message, true) &&
611
611
  typeof message.text === 'string' &&
612
612
  typeof message.modelId === 'string' &&
613
+ (message.summary === undefined || typeof message.summary === 'string') &&
613
614
  (message.interrupted === undefined || message.interrupted === true) &&
614
615
  (message.providerOptions === undefined || isRecord(message.providerOptions)) &&
615
616
  (message.thinking === undefined || isAssistantThinking(message.thinking)) &&
@@ -17,6 +17,7 @@
17
17
  * under the License.
18
18
  */
19
19
  import { defineObjectShape, hasExactShape, isRecord } from './record-schema.js';
20
+ const EXTERNAL_CONNECTOR_ORIGIN_SHAPE = defineObjectShape()(['kind', 'connectorId'], ['senderId']);
20
21
  const SCHEDULED_TASK_ORIGIN_SHAPE = defineObjectShape()(['kind', 'scheduledTaskId'], []);
21
22
  const CLOUD_ACTIVATION_ORIGIN_SHAPE = defineObjectShape()(['kind', 'activationId'], []);
22
23
  const LEGACY_AUTOMATION_ORIGIN_SHAPE = defineObjectShape()(['kind', 'automationId'], []);
@@ -26,6 +27,19 @@ const AGENT_GRAPH_ORIGIN_SHAPE = defineObjectShape()(['kind', 'graphId', 'wakeId
26
27
  export function decodeTurnOrigin(value) {
27
28
  if (!isRecord(value))
28
29
  return undefined;
30
+ if (value.kind === 'external_connector') {
31
+ if (!hasExactShape(value, EXTERNAL_CONNECTOR_ORIGIN_SHAPE) ||
32
+ typeof value.connectorId !== 'string' ||
33
+ !value.connectorId.length ||
34
+ (value.senderId !== undefined &&
35
+ (typeof value.senderId !== 'string' || !value.senderId.length)))
36
+ return undefined;
37
+ return {
38
+ kind: 'external_connector',
39
+ connectorId: value.connectorId,
40
+ ...(value.senderId !== undefined ? { senderId: value.senderId } : {}),
41
+ };
42
+ }
29
43
  if (value.kind === 'workhub_result') {
30
44
  const keys = ['kind', 'eventId', 'actionId', 'delegationId', 'targetSessionId', 'targetTurnId'];
31
45
  if (Object.keys(value).length !== keys.length ||
@@ -1748,6 +1748,7 @@ export class AiSdkTurn {
1748
1748
  throw providerOutcome.failure;
1749
1749
  finishReason = providerOutcome.finishReason;
1750
1750
  await queue.waitUntilConsumedThroughCurrent();
1751
+ let toolRequestedFinish = false;
1751
1752
  if (returnedToolCalls.length > 0) {
1752
1753
  const continuationBudgetRemains = maxSteps === undefined || runtimeSteps < maxSteps;
1753
1754
  if (continuationBudgetRemains && !this.deps.backend.loadTurnRuntimeEvents) {
@@ -1819,6 +1820,12 @@ export class AiSdkTurn {
1819
1820
  throw outcome.reason;
1820
1821
  const settlement = outcome.value;
1821
1822
  const toolCall = returnedToolCalls[index];
1823
+ const definition = toolCall && toolsByName.get(toolCall.toolName);
1824
+ if (returnedToolCalls.length === 1 &&
1825
+ definition?.executionSemantics === 'exclusive_step' &&
1826
+ definition.finishTurnOnSuccess &&
1827
+ !settlement.providerError)
1828
+ toolRequestedFinish = true;
1822
1829
  if (isPlanToolResult(settlement.result)) {
1823
1830
  this.handlePlanToolResult(settlement.result, queue);
1824
1831
  }
@@ -1851,6 +1858,21 @@ export class AiSdkTurn {
1851
1858
  ...(providerStepUsage ? { usage: providerStepUsage } : {}),
1852
1859
  });
1853
1860
  lastCompletedStepHadToolResult = returnedToolCalls.length > 0;
1861
+ // Finish only after the tool result is durably consumed. The Host can
1862
+ // veto completion (for example, new Inbox input raced with Sleep).
1863
+ if (toolRequestedFinish && !this.aborted && !this.loopStopRequested) {
1864
+ const decision = await this.deps.backend.beforeTurnFinish?.({
1865
+ sessionId: this.deps.backend.sessionId,
1866
+ turnId,
1867
+ signal: turnAbortController.signal,
1868
+ });
1869
+ if (!decision || decision.allow) {
1870
+ finishReason = 'stop';
1871
+ this.deps.modelAdapter.clearContinuation(this.turnId);
1872
+ break agentLoop;
1873
+ }
1874
+ finishFeedback = decision.feedback;
1875
+ }
1854
1876
  const stepLimitReached = maxSteps !== undefined && runtimeSteps >= maxSteps;
1855
1877
  if (sandboxBoundaryFinalizationStep ||
1856
1878
  (stepLimitReached &&
@@ -100,7 +100,13 @@ export class PluginAgentService extends Service {
100
100
  return Object.freeze({
101
101
  ...descriptor,
102
102
  id,
103
- followup: (message) => service.runtime().followup(id, message, invocation),
103
+ followup: (message, options) => service.runtime().followup(id, message, invocation, options),
104
+ messageExecution: async (messageId) => {
105
+ const runtime = service.runtime();
106
+ if (!runtime.messageExecution)
107
+ throw new Error('Runtime message execution query unavailable');
108
+ return runtime.messageExecution(id, messageId, invocation);
109
+ },
104
110
  steer: (message) => service.runtime().steer(id, message, invocation),
105
111
  inject: (message) => service.runtime().inject(id, message, invocation),
106
112
  cancel: () => service.runtime().cancel(id, invocation),
@@ -90,6 +90,7 @@ export function backfillRuntimeEventsFromStoredMessages(input) {
90
90
  content: {
91
91
  kind: 'text',
92
92
  text: message.text,
93
+ ...(message.summary !== undefined ? { summary: message.summary } : {}),
93
94
  ...(message.interrupted ? { interrupted: true } : {}),
94
95
  },
95
96
  actions: { stateDelta: recoveryState(now, message) },
@@ -659,6 +659,7 @@ function projectText(event, state, messages) {
659
659
  turnId: event.turnId,
660
660
  ts: event.ts,
661
661
  text: event.content.text,
662
+ ...(event.content.summary !== undefined ? { summary: event.content.summary } : {}),
662
663
  ...(event.content.interrupted ? { interrupted: true } : {}),
663
664
  ...(event.content.providerOptions !== undefined
664
665
  ? { providerOptions: structuredClone(event.content.providerOptions) }
@@ -0,0 +1,89 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { createHash } from 'node:crypto';
20
+ import { z } from 'zod';
21
+ import { validateImageBytes } from '@maka/runtime/image-file';
22
+ import { assistantAvatarSchema, assistantEmojiSchema, DEFAULT_ASSISTANT_AVATAR, } from '../../protocol/personal-assistant.js';
23
+ export function initializeAvatar(ctx, store, bot, register) {
24
+ const current = () => store.get('avatar');
25
+ const avatar = () => {
26
+ const { imageData: _, ...metadata } = current() ?? {};
27
+ return assistantAvatarSchema.safeParse(metadata).data ?? DEFAULT_ASSISTANT_AVATAR;
28
+ };
29
+ ctx.api.register('assistant.avatar', () => ({ avatar: avatar() }));
30
+ // Bounded transfers keep large images out of the normal status polling and model transcript.
31
+ ctx.api.register('assistant.avatar-image', (raw) => {
32
+ const { id, offset } = z
33
+ .object({ id: z.string(), offset: z.number().int().nonnegative().default(0) })
34
+ .parse(raw);
35
+ const saved = current();
36
+ if (avatar().image?.id !== id || !saved?.imageData)
37
+ throw Error('Avatar changed; refresh its metadata');
38
+ if (offset >= saved.imageData.length)
39
+ throw Error('Invalid avatar offset');
40
+ const data = saved.imageData.slice(offset, offset + 192 * 1024);
41
+ return {
42
+ id,
43
+ offset,
44
+ data,
45
+ nextOffset: offset + data.length < saved.imageData.length ? offset + data.length : null,
46
+ };
47
+ });
48
+ register('AssistantSetAvatar', '更换你自己的头像,入口和聊天消息同步显示,重启后保留。image 填生成或找到的图片的当前工具会话工作目录内的本地路径或公网 HTTP(S) 图片网址(PNG/JPEG/WebP/GIF,最多 2 MiB),不要传 base64。也可用 emoji 填一个 emoji;两者选一。background 可填 #RRGGBB。图片会保存副本;此工具只设置头像,生成或找图使用已有工具,必要时委派任务。不会发送消息。', z
49
+ .object({
50
+ emoji: assistantEmojiSchema.optional(),
51
+ image: z.string().trim().min(1).max(8192).optional(),
52
+ background: z
53
+ .string()
54
+ .regex(/^#[0-9a-fA-F]{6}$/)
55
+ .default('#E5EDFF'),
56
+ })
57
+ .strict()
58
+ .refine((value) => Boolean(value.emoji) !== Boolean(value.image), 'Provide exactly one of emoji or image'), async (input, call) => {
59
+ let saved;
60
+ if (input.image) {
61
+ if (!ctx.readAvatarImage)
62
+ throw Error('Image loading is unavailable on this Host');
63
+ const worker = bot.worker();
64
+ const owner = bot.owner();
65
+ const result = await ctx.readAvatarImage(call.sessionId, input.image, call.abortSignal);
66
+ call.abortSignal.throwIfAborted();
67
+ bot.ready();
68
+ if (bot.worker() !== worker || bot.owner() !== owner)
69
+ throw Error('Assistant binding changed');
70
+ const image = validateImageBytes(result.bytes, 'chat');
71
+ const bytes = Buffer.from(image.bytes);
72
+ const imageData = bytes.toString('base64');
73
+ saved = {
74
+ background: input.background,
75
+ image: {
76
+ id: createHash('sha256').update(bytes).digest('hex'),
77
+ mimeType: image.mimeType,
78
+ length: imageData.length,
79
+ },
80
+ imageData,
81
+ };
82
+ }
83
+ else
84
+ saved = { emoji: input.emoji, background: input.background };
85
+ // One SQLite value makes image content and metadata an atomic replacement.
86
+ store.set('avatar', saved);
87
+ return { updated: true, avatar: avatar() };
88
+ });
89
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /** In-process routing only; durable mailbox and wake state belong to the Bot. */
20
+ export class AssistantBotBridge {
21
+ endpoint;
22
+ register(endpoint) {
23
+ this.endpoint = endpoint;
24
+ }
25
+ status() {
26
+ return this.endpoint?.status() ?? null;
27
+ }
28
+ isConversation(id) {
29
+ return this.endpoint?.owner() === id;
30
+ }
31
+ hasUserMessage(sessionId, id) {
32
+ return this.isConversation(sessionId) && this.endpoint.hasUserMessage(id);
33
+ }
34
+ isWorker(id) {
35
+ return this.endpoint?.worker() === id;
36
+ }
37
+ ownerFor(id) {
38
+ return this.isWorker(id) ? this.endpoint?.owner() : undefined;
39
+ }
40
+ async receive(sessionId, id, content, origin) {
41
+ if (!this.isConversation(sessionId))
42
+ return false;
43
+ await this.endpoint.receive(id, content, origin);
44
+ return true;
45
+ }
46
+ post(owner, input) {
47
+ if (!this.isConversation(owner))
48
+ return false;
49
+ this.endpoint.post(input);
50
+ return true;
51
+ }
52
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ export const BOT_TOOLS = new Set([
20
+ 'InboxList',
21
+ 'InboxRead',
22
+ 'InboxCheck',
23
+ 'SendUser',
24
+ 'AssistantSetAvatar',
25
+ 'Sleep',
26
+ 'AssistantConversation',
27
+ 'MatterDelegate',
28
+ 'MatterTasks',
29
+ 'MatterTaskMessage',
30
+ 'MatterReply',
31
+ 'MatterTaskControl',
32
+ 'MatterDelegationRecover',
33
+ 'MatterFeedbackRetry',
34
+ 'InitiativeEnable',
35
+ 'InitiativeConfigure',
36
+ 'InitiativeControl',
37
+ 'InitiativeStatus',
38
+ 'MemoryIndexCreate',
39
+ 'MemoryIndexCreateGroup',
40
+ 'MemoryIndexMaintain',
41
+ 'MemoryIndexControl',
42
+ 'MemoryIndexAddSources',
43
+ 'MemoryIndexList',
44
+ 'MemoryIndexRead',
45
+ 'MemoryIndexContent',
46
+ 'MemoryIndexLinks',
47
+ 'MemoryOriginal',
48
+ 'MemoryHistory',
49
+ 'MemorySources',
50
+ 'MemoryRange',
51
+ ]);
52
+ export const BOT_DELEGATION_TOOLS = new Set([...BOT_TOOLS].filter((name) => name.startsWith('Matter') || name.startsWith('Initiative') || name.startsWith('Memory')));
@@ -0,0 +1,300 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { randomUUID } from 'node:crypto';
20
+ import { setTimeout as delay } from 'node:timers/promises';
21
+ import { ASSISTANT_BACKGROUND_SESSION_LABEL } from '@maka/core/session';
22
+ const UNREAD_SLEEP_FEEDBACK = '本次未能休眠:你读取 Inbox 后又收到新消息,或上一批还有未投递消息。请调用 InboxCheck 接收正文,再自行决定如何处理;不要求回复或完成每件事,之后可再次调用 Sleep。';
23
+ export class BotController {
24
+ ctx;
25
+ binding;
26
+ store;
27
+ abort = new AbortController();
28
+ timer;
29
+ running;
30
+ flushing;
31
+ activeAgent;
32
+ deliveredThrough = 0;
33
+ deliveries = new Map();
34
+ checkInbox(callId, limit) {
35
+ this.ready();
36
+ const previous = this.deliveries.get(callId);
37
+ if (previous)
38
+ return previous;
39
+ const batch = this.store.batch(this.deliveredThrough, limit);
40
+ this.deliveredThrough = batch.through;
41
+ this.deliveries.set(callId, batch);
42
+ return batch;
43
+ }
44
+ isWorking() {
45
+ return !!this.activeAgent && !this.store.get('sleepTurn') && !this.store.get('error');
46
+ }
47
+ isSleeping() {
48
+ return (!this.running &&
49
+ !this.store.get('error') &&
50
+ this.store.head() <= (this.store.get('deliveredThrough') ?? 0));
51
+ }
52
+ constructor(ctx, binding, store) {
53
+ this.ctx = ctx;
54
+ this.binding = binding;
55
+ this.store = store;
56
+ }
57
+ owner() {
58
+ return this.binding.get()?.sessionId;
59
+ }
60
+ worker() {
61
+ return this.store.get('worker');
62
+ }
63
+ invocation(sessionId = this.owner()) {
64
+ return {
65
+ sessionId,
66
+ cwd: this.binding.get().cwd,
67
+ emitOutput: () => { },
68
+ turnId: 'assistant-bot',
69
+ toolCallId: randomUUID(),
70
+ abortSignal: this.abort.signal,
71
+ };
72
+ }
73
+ start() {
74
+ this.timer = setInterval(() => this.tick(), 1000);
75
+ this.timer.unref?.();
76
+ this.tick();
77
+ }
78
+ ready() {
79
+ this.abort.signal.throwIfAborted();
80
+ this.binding.fence();
81
+ }
82
+ post(input) {
83
+ this.ready();
84
+ this.store.post(input);
85
+ this.tick();
86
+ }
87
+ async receive(id, content, origin) {
88
+ this.ready();
89
+ this.store.post({
90
+ id,
91
+ type: 'user_message',
92
+ source: origin ? 'external_connector' : 'user',
93
+ content: origin ? { ...content, origin } : content,
94
+ }, {
95
+ ...content,
96
+ type: 'user',
97
+ ...(origin ? { origin } : {}),
98
+ id,
99
+ turnId: `assistant-input:${id}`,
100
+ ts: Date.now(),
101
+ });
102
+ await this.flush();
103
+ this.tick();
104
+ }
105
+ async flush() {
106
+ if (this.flushing) {
107
+ await this.flushing;
108
+ if (this.store.pendingVisible().length)
109
+ await this.flush();
110
+ return;
111
+ }
112
+ this.flushing = (async () => {
113
+ for (const message of this.store.pendingVisible()) {
114
+ this.ready();
115
+ if (!this.ctx.appendConversation)
116
+ throw Error('Assistant conversation transport unavailable');
117
+ await this.ctx.appendConversation(this.owner(), message);
118
+ this.store.delivered(message.id);
119
+ }
120
+ })().finally(() => {
121
+ this.flushing = undefined;
122
+ });
123
+ return this.flushing;
124
+ }
125
+ async agent() {
126
+ return this.ctx.agents.withInvocation(this.invocation(), async () => {
127
+ let id = this.worker();
128
+ if (!id) {
129
+ const label = `assistant-bot:${this.owner()}`;
130
+ const matches = (await this.ctx.sessionQuery.list()).filter((s) => s.labels?.includes(label));
131
+ if (matches.length > 1)
132
+ throw Error('Multiple Bot workers found; cannot choose a conversation');
133
+ id = matches[0]?.id;
134
+ if (!id)
135
+ id = (await this.ctx.agents.create({
136
+ background: true,
137
+ name: '个人助手 · Bot',
138
+ labels: [label, ASSISTANT_BACKGROUND_SESSION_LABEL],
139
+ })).sessionId;
140
+ this.store.set('worker', id);
141
+ }
142
+ return this.ctx.agents.resume({ sessionId: id });
143
+ });
144
+ }
145
+ tick() {
146
+ if (this.running || this.abort.signal.aborted || !this.owner())
147
+ return;
148
+ try {
149
+ this.ready();
150
+ }
151
+ catch {
152
+ return;
153
+ }
154
+ this.running = this.run()
155
+ .catch((error) => {
156
+ if (!this.abort.signal.aborted)
157
+ this.store.set('error', String(error));
158
+ })
159
+ .finally(() => {
160
+ this.running = undefined;
161
+ });
162
+ }
163
+ async run() {
164
+ await this.flush();
165
+ if (this.store.get('error'))
166
+ return;
167
+ if (this.store.head() <= (this.store.get('deliveredThrough') ?? 0)) {
168
+ this.store.set('wake', null);
169
+ this.store.set('wakeError', null);
170
+ return;
171
+ }
172
+ // Commit delivery only at a normal Sleep boundary. An interrupted loop may
173
+ // replay mail, but cannot lose it between a tool return and durable history.
174
+ this.deliveredThrough = this.store.get('deliveredThrough') ?? 0;
175
+ this.deliveries.clear();
176
+ const agent = await this.agent();
177
+ this.activeAgent = agent;
178
+ try {
179
+ const status = (await agent.snapshot())?.agent?.status;
180
+ if (['blocked', 'waiting_for_user'].includes(status))
181
+ throw Error('Bot requires attention in its worker Session');
182
+ let wake = this.store.get('wake');
183
+ if (wake || status !== 'running') {
184
+ if (!wake) {
185
+ this.store.set('sleepTurn', null);
186
+ wake = {
187
+ messageId: randomUUID(),
188
+ text: `Inbox 有新消息。当前 UTC 时间:${new Date().toISOString()}。用 InboxCheck 批量接收新消息正文,自主决定如何处理;不需要轮询。`,
189
+ };
190
+ // Persist before submission: an ambiguous response must not create another wake.
191
+ this.store.set('wake', wake);
192
+ }
193
+ for (;;) {
194
+ this.ready();
195
+ let resolution;
196
+ try {
197
+ resolution = await agent.messageExecution(wake.messageId);
198
+ if (resolution?.state === 'not_admitted') {
199
+ const admitted = await agent.followup(wake.text, { messageId: wake.messageId });
200
+ if (!['turn_started', 'followup'].includes(admitted?.disposition))
201
+ throw Error('Bot wake was not admitted');
202
+ if (admitted.disposition === 'turn_started') {
203
+ this.store.set('wakeError', null);
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ catch (error) {
209
+ // Retry reconciliation on the next scheduler tick, keeping the submission identity.
210
+ this.store.set('wakeError', String(error));
211
+ return;
212
+ }
213
+ this.store.set('wakeError', null);
214
+ if (resolution?.state === 'owned') {
215
+ const currentStatus = (await agent.snapshot())?.agent?.status;
216
+ if (!this.store.get('sleepTurn') &&
217
+ ['idle', 'active', 'aborted'].includes(currentStatus)) {
218
+ // An admitted wake can outlive its interrupted execution. Keep its
219
+ // history, but persist a fresh identity before resuming unread mail.
220
+ wake = { ...wake, messageId: randomUUID() };
221
+ this.store.set('wake', wake);
222
+ continue;
223
+ }
224
+ break;
225
+ }
226
+ // Pending submissions may deliberately be omitted from query results.
227
+ if (!resolution) {
228
+ await delay(1000, undefined, { signal: this.abort.signal });
229
+ continue;
230
+ }
231
+ if (resolution?.state === 'cancelled') {
232
+ this.store.set('wake', null);
233
+ throw Error('Bot wake was cancelled; Inbox is preserved');
234
+ }
235
+ if (!['pending', 'not_admitted'].includes(resolution?.state))
236
+ throw Error('Unexpected Bot wake execution state: ' + resolution?.state);
237
+ await delay(1000, undefined, { signal: this.abort.signal });
238
+ }
239
+ }
240
+ try {
241
+ await agent.whenIdle(this.abort.signal);
242
+ }
243
+ catch (error) {
244
+ // A failed execution is terminal; a failed observation is not.
245
+ if (!this.abort.signal.aborted) {
246
+ const stopped = (await agent.snapshot())?.agent?.status;
247
+ if (['active', 'idle', 'aborted'].includes(stopped))
248
+ this.store.set('wake', null);
249
+ }
250
+ throw error;
251
+ }
252
+ const finalStatus = (await agent.snapshot())?.agent?.status;
253
+ this.store.set('wake', null);
254
+ if (['aborted', 'blocked', 'waiting_for_user'].includes(finalStatus))
255
+ throw Error('Bot stopped: ' + finalStatus);
256
+ if (!this.store.get('sleepTurn'))
257
+ throw Error('Bot ended without Sleep; send a new message to retry');
258
+ }
259
+ finally {
260
+ this.activeAgent = undefined;
261
+ }
262
+ }
263
+ sleep(turnId) {
264
+ this.ready();
265
+ if (this.store.head() > this.deliveredThrough) {
266
+ this.store.set('sleepTurn', null);
267
+ return { sleeping: false, notice: UNREAD_SLEEP_FEEDBACK };
268
+ }
269
+ this.store.transaction(() => {
270
+ this.store.set('sleepThrough', this.deliveredThrough);
271
+ this.store.set('sleepTurn', turnId);
272
+ });
273
+ return {
274
+ sleeping: true,
275
+ notice: '本轮停止工作;新消息将唤醒你。已投递消息仍可回查,不会因未回复而重复唤醒。',
276
+ };
277
+ }
278
+ beforeFinish(turnId) {
279
+ this.ready();
280
+ if (this.store.get('sleepTurn') === turnId &&
281
+ this.store.head() <= (this.store.get('sleepThrough') ?? 0)) {
282
+ this.store.set('deliveredThrough', this.deliveredThrough);
283
+ return { allow: true };
284
+ }
285
+ this.store.set('sleepTurn', null);
286
+ return {
287
+ allow: false,
288
+ feedback: this.store.head() > this.deliveredThrough
289
+ ? UNREAD_SLEEP_FEEDBACK
290
+ : '你是 Inbox 驱动的 Bot。普通文本不会发送给用户,也不会让你休眠。需要交流用 SendUser,无需继续时调用 Sleep。若 Inbox 还有未投递消息,用 InboxCheck 接收正文后自行决定如何处理;读取不要求回复或完成事情。',
291
+ };
292
+ }
293
+ async close() {
294
+ clearInterval(this.timer);
295
+ this.abort.abort(Error('Assistant Bot stopped'));
296
+ if (this.activeAgent)
297
+ await this.activeAgent.cancel();
298
+ await Promise.allSettled([this.running, this.flushing].filter(Boolean));
299
+ }
300
+ }