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.
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/runtime-event.js +3 -0
- package/node_modules/@maka/core/dist/session.js +2 -1
- package/node_modules/@maka/core/dist/turn-origin.js +14 -0
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +22 -0
- package/node_modules/@maka/runtime/dist/plugin-agent-service.js +7 -1
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -0
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
- package/node_modules/@maka/runtime-host/dist/assistant/bot/avatar.js +89 -0
- package/node_modules/@maka/runtime-host/dist/assistant/bot/bridge.js +52 -0
- package/node_modules/@maka/runtime-host/dist/assistant/bot/capabilities.js +52 -0
- package/node_modules/@maka/runtime-host/dist/assistant/bot/controller.js +300 -0
- package/node_modules/@maka/runtime-host/dist/assistant/bot/conversation.js +116 -0
- package/node_modules/@maka/runtime-host/dist/assistant/bot/host.js +133 -0
- package/node_modules/@maka/runtime-host/dist/assistant/bot/store.js +223 -0
- package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/assistant.js +3 -1
- package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/controller.js +61 -4
- package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/host.js +88 -14
- package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/prompt.js +1 -2
- package/node_modules/@maka/runtime-host/dist/assistant/index-initiative/store.js +4 -3
- package/node_modules/@maka/runtime-host/dist/assistant/memory-network/controller.js +185 -29
- package/node_modules/@maka/runtime-host/dist/assistant/memory-network/host.js +18 -11
- package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/clarification.js +124 -0
- package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/controller.js +23 -2
- package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/delegation.js +29 -11
- package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/delivery.js +11 -0
- package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/host.js +2 -7
- package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/prompt.js +1 -1
- package/node_modules/@maka/runtime-host/dist/assistant/proactive-matters/store.js +5 -3
- package/node_modules/@maka/runtime-host/dist/assistant/service.js +23 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +7 -1
- package/node_modules/@maka/runtime-host/dist/protocol/message.js +20 -1
- package/node_modules/@maka/runtime-host/dist/protocol/personal-assistant.js +33 -0
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +3 -1
- package/node_modules/@maka/runtime-host/dist/server/avatar-image-source.js +66 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +154 -33
- package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +3 -2
- package/node_modules/@maka/storage/dist/agent-run-store-contract.js +3 -1
- package/node_modules/@maka/storage/dist/cursor-session-adapter.js +604 -0
- package/node_modules/@maka/storage/dist/external-session-adapters.js +2 -0
- package/package.json +1 -1
|
Binary file
|
|
@@ -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
|
+
}
|