larkin 0.4.8 → 0.4.10
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/README.md +2 -0
- package/dist/agent/context-prompt.cjs +2 -1
- package/dist/agent/context-prompt.mjs +2 -1
- package/dist/app/cli.mjs +2 -2
- package/dist/app/runtime-agent-config.mjs +7 -3
- package/dist/app/setup.mjs +11 -0
- package/dist/feishu/host-shell.cjs +4 -3
- package/dist/feishu/host-shell.mjs +4 -3
- package/dist/feishu/platform-hosts.cjs +49 -0
- package/dist/feishu/platform-hosts.mjs +40 -0
- package/dist/runtime/pi-subagents-notification.mjs +161 -0
- package/dist/runtime/runtime-adapters.mjs +32 -1
- package/dist/runtime/runtime-host.cjs +250 -1
- package/dist/runtime/runtime-host.mjs +250 -1
- package/dist/setup/bot-register.mjs +50 -4
- package/dist/setup/grant-scopes.mjs +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,8 @@ Feishu clients do not reliably render Markdown links such as `[label](URL)` as c
|
|
|
87
87
|
|
|
88
88
|
## Setup and configuration
|
|
89
89
|
|
|
90
|
+
Feishu (https://open.feishu.cn) and Lark (https://open.larksuite.com) are different platforms; `larkin setup` must be told which brand with `--tenant feishu|lark` or the interactive prompt before the authorization QR, and must never emit a `feishu.cn` host for a Lark tenant.
|
|
91
|
+
|
|
90
92
|
During setup, a new Agent is offered Pi first, followed by Codex and Claude Code. Provider keys are stored only in the selected Agent's private provider directory, not in the ordinary Agent config. Setup also discovers every API-key and OAuth/subscription login exposed by the pinned official Pi registry and delegates those flows to Pi.
|
|
91
93
|
|
|
92
94
|
Use `larkin pi-auth status` or `larkin pi-auth logout <provider>` to manage Pi auth. The builtin Pi runtime loads Larkin's two supported extensions inline: background subagents and the 60-second foreground bash timeout guard are enabled without writing extension files or arguments. A compatible external Pi installation keeps the existing explicit `-e` extension path.
|
|
@@ -16,7 +16,7 @@ const agent_cli_capabilities_js_1 = require("./agent-cli-capabilities.cjs");
|
|
|
16
16
|
* 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
|
|
17
17
|
* 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
|
|
18
18
|
*/
|
|
19
|
-
exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-
|
|
19
|
+
exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v21";
|
|
20
20
|
/**
|
|
21
21
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
22
22
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -126,6 +126,7 @@ class ContextPromptBuilder {
|
|
|
126
126
|
"## Feishu IM command map",
|
|
127
127
|
"",
|
|
128
128
|
`Use only the Larkin-owned \`${executable}\` command for Feishu. Bot identity, private configuration, and freshness are Runtime-bound; the wrapper delegates to the unmodified global official CLI. Use \`${executable} <command> --help\`, never invoke bare \`lark-cli\`, and never pass \`--agent\`, \`--as user\`, \`--profile\`, or \`--config-dir\`. If a command reports a missing scope, relay that error unchanged and ask the user to authorize it; do not bypass the scope boundary.`,
|
|
129
|
+
"If a platform URL must be shown, use the current Agent tenant host; never emit feishu.cn for a Lark tenant.",
|
|
129
130
|
`Use the exact Inbox target for history reads. For \`thread:<chat_id>:<thread_id>\`, run \`${executable} im +threads-messages-list --thread <thread_id> --order desc --page-size 10 --no-reactions --json\`. For \`chat:<chat_id>\`, run \`${executable} im +chat-messages-list --chat-id <chat_id> --order desc --page-size 10 --no-reactions --json\`.`,
|
|
130
131
|
"Successful history response messages are always at `data.messages`. Never use a chat-wide fallback for a thread target, never merge stderr with `2>&1` before parsing JSON, and never truncate structured output before parsing it.",
|
|
131
132
|
"If the scoped history read fails or its schema is invalid, fail visibly. Do not reuse remembered or hard-coded text to make the task appear successful.",
|
|
@@ -13,7 +13,7 @@ import { agentCliPromptCapabilities } from "./agent-cli-capabilities.mjs";
|
|
|
13
13
|
* 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
|
|
14
14
|
* 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
|
|
15
15
|
*/
|
|
16
|
-
export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-
|
|
16
|
+
export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v21";
|
|
17
17
|
/**
|
|
18
18
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
19
19
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -123,6 +123,7 @@ export class ContextPromptBuilder {
|
|
|
123
123
|
"## Feishu IM command map",
|
|
124
124
|
"",
|
|
125
125
|
`Use only the Larkin-owned \`${executable}\` command for Feishu. Bot identity, private configuration, and freshness are Runtime-bound; the wrapper delegates to the unmodified global official CLI. Use \`${executable} <command> --help\`, never invoke bare \`lark-cli\`, and never pass \`--agent\`, \`--as user\`, \`--profile\`, or \`--config-dir\`. If a command reports a missing scope, relay that error unchanged and ask the user to authorize it; do not bypass the scope boundary.`,
|
|
126
|
+
"If a platform URL must be shown, use the current Agent tenant host; never emit feishu.cn for a Lark tenant.",
|
|
126
127
|
`Use the exact Inbox target for history reads. For \`thread:<chat_id>:<thread_id>\`, run \`${executable} im +threads-messages-list --thread <thread_id> --order desc --page-size 10 --no-reactions --json\`. For \`chat:<chat_id>\`, run \`${executable} im +chat-messages-list --chat-id <chat_id> --order desc --page-size 10 --no-reactions --json\`.`,
|
|
127
128
|
"Successful history response messages are always at `data.messages`. Never use a chat-wide fallback for a thread target, never merge stderr with `2>&1` before parsing JSON, and never truncate structured output before parsing it.",
|
|
128
129
|
"If the scoped history read fails or its schema is invalid, fail visibly. Do not reuse remembered or hard-coded text to make the task appear successful.",
|
package/dist/app/cli.mjs
CHANGED
|
@@ -49,8 +49,8 @@ if (runtimeAgentAuthority && command === "comment")
|
|
|
49
49
|
const commandHelp = {
|
|
50
50
|
start: `Usage: larkin start [--agent <App ID> | --agents <App ID,...>]
|
|
51
51
|
Start one foreground supervisor for the daemon and local dashboard, or reuse it.`,
|
|
52
|
-
setup: `Usage: larkin setup [--runtime <builtin-pi|external-pi|codex|claude>] [--provider <id> --api-key <key>]
|
|
53
|
-
Run setup to create or connect a bot, configure its Agent, and attach it. Interactive terminals keep the guided flow; Agent-driven (non-TTY) runs default to builtin-pi and need --provider/--api-key (or --runtime external-pi).`,
|
|
52
|
+
setup: `Usage: larkin setup [--tenant feishu|lark] [--runtime <builtin-pi|external-pi|codex|claude>] [--provider <id> --api-key <key>]
|
|
53
|
+
Run setup to create or connect a bot, configure its Agent, and attach it. Choose Feishu or Lark before the authorization QR (--tenant; default feishu). Interactive terminals keep the guided flow; Agent-driven (non-TTY) runs default to builtin-pi and need --provider/--api-key (or --runtime external-pi).`,
|
|
54
54
|
status: `Usage: larkin status [--json]
|
|
55
55
|
Show Agent configuration, bot identity, credentials, and connection status. Use --json for readiness automation.`,
|
|
56
56
|
agents: `Usage: larkin agents [--json]
|
|
@@ -9,6 +9,7 @@ import { assertSecureBotsDirectory, readSecureBotCredential, validCredentialReco
|
|
|
9
9
|
import { resolveOfficialLarkCli } from "./official-lark-cli.mjs";
|
|
10
10
|
import { internalCommandSpec } from "./internal-command.mjs";
|
|
11
11
|
import { assertAgentWorkspaceBound, larkChannelSourceConfigPath, larkChannelWorkspaceConfigPath, managedLarkCliEnv, } from "./agent-lark-cli-workspace.mjs";
|
|
12
|
+
import { accountTenantFromOpenHost, openPlatformHost, } from "../feishu/platform-hosts.mjs";
|
|
12
13
|
function assertSecureProfileDirectory(directory) {
|
|
13
14
|
const stat = fs.lstatSync(directory);
|
|
14
15
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
@@ -140,7 +141,7 @@ export function hydrateRuntimeAgent(configDir, agent) {
|
|
|
140
141
|
return {
|
|
141
142
|
...agent,
|
|
142
143
|
feishuAppSecret: credential.appSecret,
|
|
143
|
-
feishuDomain: credential.tenant
|
|
144
|
+
feishuDomain: openPlatformHost(credential.tenant),
|
|
144
145
|
credentialRevision,
|
|
145
146
|
};
|
|
146
147
|
}
|
|
@@ -248,7 +249,7 @@ export function installRuntimeCommandShims(agent) {
|
|
|
248
249
|
}
|
|
249
250
|
return commandDir;
|
|
250
251
|
}
|
|
251
|
-
function sourceProjection(agent, env) {
|
|
252
|
+
export function sourceProjection(agent, env) {
|
|
252
253
|
const configDir = path.resolve(env.LARKIN_CONFIG_DIR || "");
|
|
253
254
|
if (!configDir)
|
|
254
255
|
throw new Error("LARKIN_CONFIG_DIR required for lark-channel source projection");
|
|
@@ -261,7 +262,7 @@ function sourceProjection(agent, env) {
|
|
|
261
262
|
accounts: { app: {
|
|
262
263
|
id: agent.feishuAppId,
|
|
263
264
|
secret: { source: "exec", provider: "larkin-bot-credential", id: agent.feishuAppId },
|
|
264
|
-
tenant: agent.feishuDomain
|
|
265
|
+
tenant: accountTenantFromOpenHost(agent.feishuDomain),
|
|
265
266
|
} },
|
|
266
267
|
secrets: { providers: { "larkin-bot-credential": {
|
|
267
268
|
source: "exec", command: provider.command, args: provider.args,
|
|
@@ -294,6 +295,9 @@ function validateSourceProjection(file, agent) {
|
|
|
294
295
|
throw new Error("lark-channel source projection 不得包含 plaintext secret 字段");
|
|
295
296
|
if (root.credentialRevision !== agent.credentialRevision)
|
|
296
297
|
throw new Error(`Agent ${agent.agentId} lark-channel credential revision mismatch`);
|
|
298
|
+
if (app?.tenant !== accountTenantFromOpenHost(agent.feishuDomain)) {
|
|
299
|
+
throw new Error(`Agent ${agent.agentId} lark-channel source projection tenant mismatch`);
|
|
300
|
+
}
|
|
297
301
|
}
|
|
298
302
|
export function syncAgentProfile(agent, env, dependencies = {}) {
|
|
299
303
|
const expected = path.join(path.resolve(env.LARKIN_CONFIG_DIR || ""), "state", "agents", agent.agentId, "lark-cli-config");
|
package/dist/app/setup.mjs
CHANGED
|
@@ -82,6 +82,11 @@ Options:
|
|
|
82
82
|
--base-url <url> custom 端点(provider=custom 时必填;也可覆盖 preset 默认端点)
|
|
83
83
|
--model <id> 模型 ID;默认取 provider 预设。不同 runtime 的模型可用
|
|
84
84
|
\`larkin model\` 查看 / 切换
|
|
85
|
+
--tenant feishu|lark 授权二维码之前选择品牌。默认 feishu(accounts.feishu.cn /
|
|
86
|
+
https://open.feishu.cn)。Lark 用 --tenant lark
|
|
87
|
+
(accounts.larksuite.com / https://open.larksuite.com)。
|
|
88
|
+
交互式未指定时会先询问。飞书与 Lark 是不同平台,
|
|
89
|
+
Lark 租户不得使用 feishu.cn 主机。
|
|
85
90
|
|
|
86
91
|
setup handles browser authorization, permission grants, credential storage, Agent configuration,
|
|
87
92
|
and target-only hot attach. Each Agent is identified by its bot App ID: selecting the same bot reuses
|
|
@@ -152,6 +157,12 @@ export async function main() {
|
|
|
152
157
|
registerArgs.push("--pi-distribution", normalizedRuntime.distribution);
|
|
153
158
|
}
|
|
154
159
|
registerArgs.push("--comment-subscription", OPT.commentSubscription);
|
|
160
|
+
if (has("--tenant")) {
|
|
161
|
+
const tenant = flag("--tenant");
|
|
162
|
+
if (tenant !== "feishu" && tenant !== "lark")
|
|
163
|
+
die("--tenant 只支持 feishu 或 lark");
|
|
164
|
+
registerArgs.push("--tenant", tenant === "lark" ? "lark" : "feishu");
|
|
165
|
+
}
|
|
155
166
|
for (const name of ["--provider", "--api-key", "--base-url", "--model"]) {
|
|
156
167
|
const value = flag(name);
|
|
157
168
|
if (value)
|
|
@@ -61,6 +61,7 @@ const internal_command_js_1 = require("../app/internal-command.cjs");
|
|
|
61
61
|
const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
|
|
62
62
|
const agent_readiness_js_1 = require("../app/agent-readiness.cjs");
|
|
63
63
|
const document_comment_js_1 = require("./document-comment.cjs");
|
|
64
|
+
const platform_hosts_js_1 = require("./platform-hosts.cjs");
|
|
64
65
|
const PENDING_DOCUMENT_COMMENT_LIMIT = 256;
|
|
65
66
|
function isRecord(value) {
|
|
66
67
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -152,7 +153,7 @@ function loadAgents(env, testEventSource, larkinHome, reconcileAgentWorkspaceImp
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
if (!testEventSource && (typeof candidate.feishuAppSecret !== "string" || !candidate.feishuAppSecret.trim()
|
|
155
|
-
|| !
|
|
156
|
+
|| !(0, platform_hosts_js_1.isOpenPlatformHost)(candidate.feishuDomain))) {
|
|
156
157
|
throw new Error(`LARKIN_AGENTS_CONFIG Agent ${id} 缺少有效 channel 凭证/domain`);
|
|
157
158
|
}
|
|
158
159
|
const expectedWorkspace = node_path_1.default.join(larkinHome, "agents", id);
|
|
@@ -742,7 +743,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
742
743
|
const channel = createLarkChannel({
|
|
743
744
|
appId: agent.feishuAppId,
|
|
744
745
|
appSecret: agent.feishuAppSecret,
|
|
745
|
-
domain: agent.feishuDomain
|
|
746
|
+
domain: (0, platform_hosts_js_1.requireOpenDomain)(agent.feishuDomain),
|
|
746
747
|
source: "larkin",
|
|
747
748
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
748
749
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -857,7 +858,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
857
858
|
const channel = createLarkChannel({
|
|
858
859
|
appId: agent.feishuAppId,
|
|
859
860
|
appSecret: agent.feishuAppSecret,
|
|
860
|
-
domain: agent.feishuDomain
|
|
861
|
+
domain: (0, platform_hosts_js_1.requireOpenDomain)(agent.feishuDomain),
|
|
861
862
|
source: "larkin",
|
|
862
863
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
863
864
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -21,6 +21,7 @@ import { processCommandToken } from "../app/internal-command.mjs";
|
|
|
21
21
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
22
22
|
import { isChannelReconnecting, isRuntimeReadinessCurrent } from "../app/agent-readiness.mjs";
|
|
23
23
|
import { documentCommentMessageId, documentCommentNoticeType, projectDocumentCommentEnvelope, resolveDocumentCommentContext, } from "./document-comment.mjs";
|
|
24
|
+
import { isOpenPlatformHost, requireOpenDomain } from "./platform-hosts.mjs";
|
|
24
25
|
const PENDING_DOCUMENT_COMMENT_LIMIT = 256;
|
|
25
26
|
function isRecord(value) {
|
|
26
27
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -112,7 +113,7 @@ function loadAgents(env, testEventSource, larkinHome, reconcileAgentWorkspaceImp
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
if (!testEventSource && (typeof candidate.feishuAppSecret !== "string" || !candidate.feishuAppSecret.trim()
|
|
115
|
-
|| !
|
|
116
|
+
|| !isOpenPlatformHost(candidate.feishuDomain))) {
|
|
116
117
|
throw new Error(`LARKIN_AGENTS_CONFIG Agent ${id} 缺少有效 channel 凭证/domain`);
|
|
117
118
|
}
|
|
118
119
|
const expectedWorkspace = path.join(larkinHome, "agents", id);
|
|
@@ -702,7 +703,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
702
703
|
const channel = createLarkChannel({
|
|
703
704
|
appId: agent.feishuAppId,
|
|
704
705
|
appSecret: agent.feishuAppSecret,
|
|
705
|
-
domain: agent.feishuDomain
|
|
706
|
+
domain: requireOpenDomain(agent.feishuDomain),
|
|
706
707
|
source: "larkin",
|
|
707
708
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
708
709
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -817,7 +818,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
817
818
|
const channel = createLarkChannel({
|
|
818
819
|
appId: agent.feishuAppId,
|
|
819
820
|
appSecret: agent.feishuAppSecret,
|
|
820
|
-
domain: agent.feishuDomain
|
|
821
|
+
domain: requireOpenDomain(agent.feishuDomain),
|
|
821
822
|
source: "larkin",
|
|
822
823
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
823
824
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Official Feishu vs Lark hosts from @larksuite/cli ResolveEndpoints
|
|
3
|
+
// (open / accounts / applink) plus Open-host docs and console paths.
|
|
4
|
+
// Do not add larkoffice.com or any unofficial host.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PLATFORM_HOSTS = void 0;
|
|
7
|
+
exports.parseLarkinTenant = parseLarkinTenant;
|
|
8
|
+
exports.openPlatformHost = openPlatformHost;
|
|
9
|
+
exports.registerAppAccountsHost = registerAppAccountsHost;
|
|
10
|
+
exports.isOpenPlatformHost = isOpenPlatformHost;
|
|
11
|
+
exports.requireOpenDomain = requireOpenDomain;
|
|
12
|
+
exports.accountTenantFromOpenHost = accountTenantFromOpenHost;
|
|
13
|
+
exports.PLATFORM_HOSTS = {
|
|
14
|
+
feishu: {
|
|
15
|
+
open: "https://open.feishu.cn",
|
|
16
|
+
accounts: "https://accounts.feishu.cn",
|
|
17
|
+
applink: "https://applink.feishu.cn",
|
|
18
|
+
docs: "https://open.feishu.cn/document",
|
|
19
|
+
console: "https://open.feishu.cn/app",
|
|
20
|
+
},
|
|
21
|
+
lark: {
|
|
22
|
+
open: "https://open.larksuite.com",
|
|
23
|
+
accounts: "https://accounts.larksuite.com",
|
|
24
|
+
applink: "https://applink.larksuite.com",
|
|
25
|
+
docs: "https://open.larksuite.com/document",
|
|
26
|
+
console: "https://open.larksuite.com/app",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
function parseLarkinTenant(value) {
|
|
30
|
+
return value === "feishu" || value === "lark" ? value : null;
|
|
31
|
+
}
|
|
32
|
+
function openPlatformHost(tenant) {
|
|
33
|
+
return exports.PLATFORM_HOSTS[tenant].open;
|
|
34
|
+
}
|
|
35
|
+
function registerAppAccountsHost(tenant) {
|
|
36
|
+
// node-sdk registerApp.domain / larkDomain interpolate https://${host}
|
|
37
|
+
return new URL(exports.PLATFORM_HOSTS[tenant].accounts).host;
|
|
38
|
+
}
|
|
39
|
+
function isOpenPlatformHost(value) {
|
|
40
|
+
return value === exports.PLATFORM_HOSTS.feishu.open || value === exports.PLATFORM_HOSTS.lark.open;
|
|
41
|
+
}
|
|
42
|
+
function requireOpenDomain(value) {
|
|
43
|
+
if (isOpenPlatformHost(value))
|
|
44
|
+
return value;
|
|
45
|
+
throw new Error("missing or invalid Open Platform domain");
|
|
46
|
+
}
|
|
47
|
+
function accountTenantFromOpenHost(domain) {
|
|
48
|
+
return domain === exports.PLATFORM_HOSTS.lark.open ? "lark" : "feishu";
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Official Feishu vs Lark hosts from @larksuite/cli ResolveEndpoints
|
|
2
|
+
// (open / accounts / applink) plus Open-host docs and console paths.
|
|
3
|
+
// Do not add larkoffice.com or any unofficial host.
|
|
4
|
+
export const PLATFORM_HOSTS = {
|
|
5
|
+
feishu: {
|
|
6
|
+
open: "https://open.feishu.cn",
|
|
7
|
+
accounts: "https://accounts.feishu.cn",
|
|
8
|
+
applink: "https://applink.feishu.cn",
|
|
9
|
+
docs: "https://open.feishu.cn/document",
|
|
10
|
+
console: "https://open.feishu.cn/app",
|
|
11
|
+
},
|
|
12
|
+
lark: {
|
|
13
|
+
open: "https://open.larksuite.com",
|
|
14
|
+
accounts: "https://accounts.larksuite.com",
|
|
15
|
+
applink: "https://applink.larksuite.com",
|
|
16
|
+
docs: "https://open.larksuite.com/document",
|
|
17
|
+
console: "https://open.larksuite.com/app",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export function parseLarkinTenant(value) {
|
|
21
|
+
return value === "feishu" || value === "lark" ? value : null;
|
|
22
|
+
}
|
|
23
|
+
export function openPlatformHost(tenant) {
|
|
24
|
+
return PLATFORM_HOSTS[tenant].open;
|
|
25
|
+
}
|
|
26
|
+
export function registerAppAccountsHost(tenant) {
|
|
27
|
+
// node-sdk registerApp.domain / larkDomain interpolate https://${host}
|
|
28
|
+
return new URL(PLATFORM_HOSTS[tenant].accounts).host;
|
|
29
|
+
}
|
|
30
|
+
export function isOpenPlatformHost(value) {
|
|
31
|
+
return value === PLATFORM_HOSTS.feishu.open || value === PLATFORM_HOSTS.lark.open;
|
|
32
|
+
}
|
|
33
|
+
export function requireOpenDomain(value) {
|
|
34
|
+
if (isOpenPlatformHost(value))
|
|
35
|
+
return value;
|
|
36
|
+
throw new Error("missing or invalid Open Platform domain");
|
|
37
|
+
}
|
|
38
|
+
export function accountTenantFromOpenHost(domain) {
|
|
39
|
+
return domain === PLATFORM_HOSTS.lark.open ? "lark" : "feishu";
|
|
40
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
function escapeXml(value) {
|
|
2
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3
|
+
}
|
|
4
|
+
function extractTag(content, tagName) {
|
|
5
|
+
const match = new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`).exec(content);
|
|
6
|
+
return match ? match[1].trim() : null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Pi subagents emit terminal <status> as Done or a display label
|
|
10
|
+
* (Error: ..., Aborted (...), Stopped, Wrapped up (turn limit)).
|
|
11
|
+
* Raw tokens completed/error/aborted/stopped/steered/turn-limit are also accepted.
|
|
12
|
+
*/
|
|
13
|
+
const TERMINAL_STATUS_TOKENS = new Set([
|
|
14
|
+
"done",
|
|
15
|
+
"completed",
|
|
16
|
+
"error",
|
|
17
|
+
"aborted",
|
|
18
|
+
"stopped",
|
|
19
|
+
"steered",
|
|
20
|
+
"turn-limit",
|
|
21
|
+
"turn_limit",
|
|
22
|
+
]);
|
|
23
|
+
export function isCanonicalTerminalPiSubagentStatus(status) {
|
|
24
|
+
const normalized = status.trim().toLowerCase();
|
|
25
|
+
if (!normalized)
|
|
26
|
+
return false;
|
|
27
|
+
if (TERMINAL_STATUS_TOKENS.has(normalized))
|
|
28
|
+
return true;
|
|
29
|
+
if (normalized.startsWith("error:") || normalized.startsWith("error "))
|
|
30
|
+
return true;
|
|
31
|
+
if (normalized.startsWith("aborted"))
|
|
32
|
+
return true;
|
|
33
|
+
if (normalized.startsWith("stopped"))
|
|
34
|
+
return true;
|
|
35
|
+
if (normalized.includes("turn limit") || normalized.includes("turn-limit") || normalized.includes("turn_limit")) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (normalized.includes("steered"))
|
|
39
|
+
return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function parseTaskNotificationBlock(block) {
|
|
43
|
+
const taskId = extractTag(block, "task-id");
|
|
44
|
+
const status = extractTag(block, "status");
|
|
45
|
+
const summary = extractTag(block, "summary");
|
|
46
|
+
const result = extractTag(block, "result");
|
|
47
|
+
if (!taskId || !status || !summary || !result)
|
|
48
|
+
return null;
|
|
49
|
+
if (!isCanonicalTerminalPiSubagentStatus(status))
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
taskId,
|
|
53
|
+
toolUseId: extractTag(block, "tool-use-id") ?? undefined,
|
|
54
|
+
outputFile: extractTag(block, "output-file") ?? undefined,
|
|
55
|
+
status,
|
|
56
|
+
summary,
|
|
57
|
+
result,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function parseCanonicalNotificationContent(content) {
|
|
61
|
+
const blocks = [...content.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)]
|
|
62
|
+
.map((match) => match[1]);
|
|
63
|
+
if (blocks.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const notifications = [];
|
|
67
|
+
for (const block of blocks) {
|
|
68
|
+
const parsed = parseTaskNotificationBlock(block);
|
|
69
|
+
if (!parsed || seen.has(parsed.taskId))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(parsed.taskId);
|
|
72
|
+
notifications.push(parsed);
|
|
73
|
+
}
|
|
74
|
+
if (notifications.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
const taskIds = notifications.map((notification) => notification.taskId);
|
|
77
|
+
return { taskIds, notifications, content, key: taskIds.join("|") };
|
|
78
|
+
}
|
|
79
|
+
function collectCandidateContents(node, found = new Set()) {
|
|
80
|
+
const candidates = [];
|
|
81
|
+
const visit = (value) => {
|
|
82
|
+
if (value === null || value === undefined)
|
|
83
|
+
return;
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
for (const item of value)
|
|
86
|
+
visit(item);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value !== "object")
|
|
90
|
+
return;
|
|
91
|
+
const record = value;
|
|
92
|
+
if (record.customType === "subagent-notification" && typeof record.content === "string") {
|
|
93
|
+
if (!found.has(record.content)) {
|
|
94
|
+
found.add(record.content);
|
|
95
|
+
candidates.push(record.content);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (Array.isArray(record.content)) {
|
|
99
|
+
visit(record.content);
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(record.messages))
|
|
102
|
+
visit(record.messages);
|
|
103
|
+
if (Array.isArray(record.parts))
|
|
104
|
+
visit(record.parts);
|
|
105
|
+
};
|
|
106
|
+
visit(node);
|
|
107
|
+
return candidates;
|
|
108
|
+
}
|
|
109
|
+
export function buildCanonicalPiSubagentNotificationContent(options) {
|
|
110
|
+
const status = options.status ?? "Done";
|
|
111
|
+
const summary = options.summary ?? `Agent ${JSON.stringify("Fixture")} completed`;
|
|
112
|
+
const result = options.result ?? "Fixture result.";
|
|
113
|
+
const totalTokens = options.totalTokens ?? 12;
|
|
114
|
+
const toolUses = options.toolUses ?? 1;
|
|
115
|
+
const durationMs = options.durationMs ?? 34;
|
|
116
|
+
return [
|
|
117
|
+
"<task-notification>",
|
|
118
|
+
`<task-id>${escapeXml(options.taskId)}</task-id>`,
|
|
119
|
+
options.toolUseId ? `<tool-use-id>${escapeXml(options.toolUseId)}</tool-use-id>` : null,
|
|
120
|
+
options.outputFile ? `<output-file>${escapeXml(options.outputFile)}</output-file>` : null,
|
|
121
|
+
`<status>${escapeXml(status)}</status>`,
|
|
122
|
+
`<summary>${escapeXml(summary)}</summary>`,
|
|
123
|
+
`<result>${escapeXml(result)}</result>`,
|
|
124
|
+
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${toolUses}</tool_uses><duration_ms>${durationMs}</duration_ms></usage>`,
|
|
125
|
+
"</task-notification>",
|
|
126
|
+
].filter(Boolean).join("\n");
|
|
127
|
+
}
|
|
128
|
+
export function buildCanonicalPiSubagentAssistantMessage(options) {
|
|
129
|
+
return {
|
|
130
|
+
role: "assistant",
|
|
131
|
+
content: [{
|
|
132
|
+
type: "custom",
|
|
133
|
+
customType: "subagent-notification",
|
|
134
|
+
content: buildCanonicalPiSubagentNotificationContent(options),
|
|
135
|
+
}],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function extractCanonicalPiSubagentNotification(messages) {
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const notifications = [];
|
|
141
|
+
const contents = [];
|
|
142
|
+
for (const content of collectCandidateContents(messages)) {
|
|
143
|
+
const parsed = parseCanonicalNotificationContent(content);
|
|
144
|
+
if (!parsed)
|
|
145
|
+
continue;
|
|
146
|
+
contents.push(parsed.content);
|
|
147
|
+
for (const notification of parsed.notifications) {
|
|
148
|
+
if (seen.has(notification.taskId))
|
|
149
|
+
continue;
|
|
150
|
+
seen.add(notification.taskId);
|
|
151
|
+
notifications.push(notification);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (notifications.length === 0)
|
|
155
|
+
return null;
|
|
156
|
+
const taskIds = notifications.map((notification) => notification.taskId);
|
|
157
|
+
return { taskIds, notifications, content: contents.join("\n"), key: taskIds.join("|") };
|
|
158
|
+
}
|
|
159
|
+
export function extractCanonicalPiSubagentCompletionKeyFromMessages(messages) {
|
|
160
|
+
return extractCanonicalPiSubagentNotification(messages)?.key ?? null;
|
|
161
|
+
}
|
|
@@ -10,6 +10,7 @@ import { BUNDLED_PI_VERSION, piAgentDirectory } from "./pi-provider-config.mjs";
|
|
|
10
10
|
import { recordPiRuntimeArtifactProvenance } from "./pi-artifact-provenance.mjs";
|
|
11
11
|
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
12
12
|
import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
|
|
13
|
+
import { extractCanonicalPiSubagentCompletionKeyFromMessages } from "./pi-subagents-notification.mjs";
|
|
13
14
|
import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
|
|
14
15
|
import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
15
16
|
import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
|
|
@@ -451,6 +452,8 @@ class PiSession extends EventSession {
|
|
|
451
452
|
observedSubmitEpochs = new Set();
|
|
452
453
|
observedAcceptedEpochs = new Set();
|
|
453
454
|
observedCompletedEpochs = new Set();
|
|
455
|
+
observedBackgroundCompletionKeys = new Set();
|
|
456
|
+
pendingUnownedCompletionKeys = new Set();
|
|
454
457
|
observedAgentEndEpochs = new Set();
|
|
455
458
|
firstOutputObserved = false;
|
|
456
459
|
toolCallOpen = false;
|
|
@@ -551,6 +554,8 @@ class PiSession extends EventSession {
|
|
|
551
554
|
this.observedSubmitEpochs.clear();
|
|
552
555
|
this.observedAcceptedEpochs.clear();
|
|
553
556
|
this.observedCompletedEpochs.clear();
|
|
557
|
+
this.observedBackgroundCompletionKeys.clear();
|
|
558
|
+
this.pendingUnownedCompletionKeys.clear();
|
|
554
559
|
this.observedAgentEndEpochs.clear();
|
|
555
560
|
this.activeEpoch = null;
|
|
556
561
|
this.settleArmedEpoch = null;
|
|
@@ -600,11 +605,25 @@ class PiSession extends EventSession {
|
|
|
600
605
|
this.observedCompletedEpochs.add(this.activeEpoch);
|
|
601
606
|
this.emitObservation("completed");
|
|
602
607
|
}
|
|
608
|
+
const completionNotificationKey = extractCanonicalPiSubagentCompletionKeyFromMessages(event.messages);
|
|
609
|
+
if (completionNotificationKey
|
|
610
|
+
&& this.activeEpoch === null
|
|
611
|
+
&& !this.observedBackgroundCompletionKeys.has(completionNotificationKey)) {
|
|
612
|
+
// agent_end can still have an active Pi session. Prompting before
|
|
613
|
+
// unowned agent_settled is rejected as "Agent is already processing".
|
|
614
|
+
this.pendingUnownedCompletionKeys.add(completionNotificationKey);
|
|
615
|
+
}
|
|
603
616
|
}
|
|
604
617
|
else if (event?.type === "agent_settled") {
|
|
605
618
|
const epoch = this.activeEpoch;
|
|
606
|
-
if (epoch === null
|
|
619
|
+
if (epoch === null) {
|
|
620
|
+
this.flushPendingUnownedCompletions();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (this.settleArmedEpoch !== epoch) {
|
|
624
|
+
this.flushPendingUnownedCompletions();
|
|
607
625
|
return;
|
|
626
|
+
}
|
|
608
627
|
this.activeEpoch = null;
|
|
609
628
|
this.settleArmedEpoch = null;
|
|
610
629
|
const error = this.finalAssistantError;
|
|
@@ -645,6 +664,7 @@ class PiSession extends EventSession {
|
|
|
645
664
|
this.observedAcceptedEpochs.delete(epoch);
|
|
646
665
|
this.observedCompletedEpochs.delete(epoch);
|
|
647
666
|
this.observedAgentEndEpochs.delete(epoch);
|
|
667
|
+
this.flushPendingUnownedCompletions();
|
|
648
668
|
}
|
|
649
669
|
else if (event?.type === "tool_execution_start") {
|
|
650
670
|
if (!this.firstOutputObserved) {
|
|
@@ -672,6 +692,15 @@ class PiSession extends EventSession {
|
|
|
672
692
|
this.emit({ type: "activity", activity: kind, text: String(event.assistantMessageEvent.delta) });
|
|
673
693
|
}
|
|
674
694
|
}
|
|
695
|
+
flushPendingUnownedCompletions() {
|
|
696
|
+
for (const completionKey of this.pendingUnownedCompletionKeys) {
|
|
697
|
+
if (this.observedBackgroundCompletionKeys.has(completionKey))
|
|
698
|
+
continue;
|
|
699
|
+
this.observedBackgroundCompletionKeys.add(completionKey);
|
|
700
|
+
this.emitObservation("completed", { completionKey });
|
|
701
|
+
}
|
|
702
|
+
this.pendingUnownedCompletionKeys.clear();
|
|
703
|
+
}
|
|
675
704
|
emitObservation(phase, fields = {}) {
|
|
676
705
|
const inputId = this.oldestOwnedInput();
|
|
677
706
|
const observation = { type: "runtime-observation", runtime: "pi",
|
|
@@ -681,6 +710,8 @@ class PiSession extends EventSession {
|
|
|
681
710
|
Object.defineProperty(observation, "sessionId", { value: this.sessionId, enumerable: false });
|
|
682
711
|
if (inputId)
|
|
683
712
|
Object.defineProperty(observation, "inputId", { value: inputId, enumerable: false });
|
|
713
|
+
if (fields.completionKey)
|
|
714
|
+
Object.defineProperty(observation, "completionKey", { value: fields.completionKey, enumerable: false });
|
|
684
715
|
this.emit(observation);
|
|
685
716
|
}
|
|
686
717
|
oldestOwnedInput() {
|