@ai-setting/roy-agent-core 1.7.66 → 1.7.67

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 (37) hide show
  1. package/dist/env/agent/agent-component.d.ts +29 -0
  2. package/dist/env/agent/agent-component.js +1 -1
  3. package/dist/env/agent/index.js +1 -1
  4. package/dist/env/agent/types.d.ts +16 -0
  5. package/dist/env/environment.d.ts +13 -0
  6. package/dist/env/event-source/event-source-handlers.js +4 -3
  7. package/dist/env/event-source/index.js +5 -4
  8. package/dist/env/event-source/timer-handler.d.ts +93 -1
  9. package/dist/env/index.js +10 -9
  10. package/dist/env/prompt/index.js +3 -2
  11. package/dist/env/prompt/prompts-index.d.ts +2 -7
  12. package/dist/env/task/delegate/delegate-tool.d.ts +21 -1
  13. package/dist/env/task/delegate/delegate-tool.js +3 -2
  14. package/dist/env/task/delegate/index.js +3 -2
  15. package/dist/env/task/index.js +5 -4
  16. package/dist/env/task/mcp/index.js +2 -1
  17. package/dist/env/task/plugins/__e2e__/laya-completion-e2e.d.ts +16 -0
  18. package/dist/env/task/plugins/index.js +3 -2
  19. package/dist/env/task/plugins/laya-completion-plugin.d.ts +133 -0
  20. package/dist/env/task/plugins/laya-port.d.ts +23 -0
  21. package/dist/env/task/task-component.d.ts +43 -0
  22. package/dist/env/task/tools/completion-copy.d.ts +27 -0
  23. package/dist/env/task/tools/index.js +2 -1
  24. package/dist/index.js +11 -10
  25. package/dist/shared/@ai-setting/{roy-agent-core-nr2m4xw1.js → roy-agent-core-1k83dq3a.js} +13 -3
  26. package/dist/shared/@ai-setting/{roy-agent-core-4882qsjx.js → roy-agent-core-1kxf910n.js} +1 -1
  27. package/dist/shared/@ai-setting/roy-agent-core-6m4bw8jy.js +6 -0
  28. package/dist/shared/@ai-setting/roy-agent-core-8zas6w5q.js +1083 -0
  29. package/dist/shared/@ai-setting/{roy-agent-core-v8bz7d8x.js → roy-agent-core-b7jbqfsy.js} +24 -10
  30. package/dist/shared/@ai-setting/{roy-agent-core-2wxbx36p.js → roy-agent-core-dnjm17sa.js} +21 -2
  31. package/dist/shared/@ai-setting/{roy-agent-core-t0ktaevq.js → roy-agent-core-h5vmff8w.js} +60 -4
  32. package/dist/shared/@ai-setting/{roy-agent-core-j1kksatb.js → roy-agent-core-hfr6bzts.js} +67 -10
  33. package/dist/shared/@ai-setting/{roy-agent-core-8nksrtnf.js → roy-agent-core-k4ps1hxp.js} +201 -16
  34. package/dist/shared/@ai-setting/{roy-agent-core-vs5gxgwh.js → roy-agent-core-ma35jyd9.js} +14 -3
  35. package/dist/shared/@ai-setting/{roy-agent-core-cvm1xgmg.js → roy-agent-core-wksgtqf9.js} +1 -1
  36. package/package.json +1 -1
  37. package/dist/shared/@ai-setting/roy-agent-core-8mjnb7xc.js +0 -654
@@ -10,7 +10,7 @@ import {
10
10
  } from "./roy-agent-core-1pg0fepg.js";
11
11
  import {
12
12
  WORKFLOW_EXTRACT_PROMPT
13
- } from "./roy-agent-core-vs5gxgwh.js";
13
+ } from "./roy-agent-core-ma35jyd9.js";
14
14
  import {
15
15
  assertNoLeakage,
16
16
  buildHintText,
@@ -929,6 +929,7 @@ function truncateSnippet(text) {
929
929
  var DEFAULT_TIMEOUT_MS = 2000;
930
930
  var DEFAULT_MAX_STRIKES = 3;
931
931
  var DEFAULT_MODEL = "task-decision";
932
+ var DEFAULT_GOAL_MODEL = "task-completion";
932
933
  var SYSTEMONE_PATH = "/v1/systemone";
933
934
 
934
935
  class LayaDecisionPort {
@@ -1058,19 +1059,32 @@ function isTimeoutError(err) {
1058
1059
  }
1059
1060
  return false;
1060
1061
  }
1062
+ var LAYA_DECISION_PROVIDER = "laya-decision";
1063
+ var LAYA_GOAL_DECISION_PROVIDER = "laya-goal-decision";
1064
+ function providerNameForPurpose(purpose) {
1065
+ return purpose === "goal" ? LAYA_GOAL_DECISION_PROVIDER : LAYA_DECISION_PROVIDER;
1066
+ }
1061
1067
  async function loadLayaAuth(authPath = `${process.env.HOME ?? "~"}/.roy-agent/auth.json`) {
1068
+ return loadLayaAuthForPurpose(authPath, "create");
1069
+ }
1070
+ async function loadLayaAuthForPurpose(authPath = `${process.env.HOME ?? "~"}/.roy-agent/auth.json`, purpose = "create") {
1062
1071
  try {
1063
1072
  const fs = await import("fs/promises");
1064
1073
  const raw = await fs.readFile(authPath, "utf-8");
1065
1074
  const parsed = JSON.parse(raw);
1066
- const p = parsed.providers?.["laya-decision"];
1067
- if (!p?.baseURL || !p?.apiKey)
1068
- return null;
1069
- return {
1070
- baseURL: p.baseURL,
1071
- apiKey: p.apiKey,
1072
- model: p.model ?? DEFAULT_MODEL
1073
- };
1075
+ const primary = providerNameForPurpose(purpose);
1076
+ const candidates = purpose === "goal" ? [primary, LAYA_DECISION_PROVIDER] : [primary];
1077
+ for (const name of candidates) {
1078
+ const p = parsed.providers?.[name];
1079
+ if (p?.baseURL && p?.apiKey) {
1080
+ return {
1081
+ baseURL: p.baseURL,
1082
+ apiKey: p.apiKey,
1083
+ model: p.model ?? (name === LAYA_GOAL_DECISION_PROVIDER ? DEFAULT_GOAL_MODEL : DEFAULT_MODEL)
1084
+ };
1085
+ }
1086
+ }
1087
+ return null;
1074
1088
  } catch {
1075
1089
  return null;
1076
1090
  }
@@ -2937,4 +2951,4 @@ __legacyDecorateClassTS([
2937
2951
  function createPerTaskGroupChatPlugin(config = {}) {
2938
2952
  return new PerTaskGroupChatPlugin(config);
2939
2953
  }
2940
- export { createAutoTaskPlugin, LAYA_REUSE_THRESHOLD, LayaDecisionPlugin, createLayaDecisionPlugin, WORKFLOW_EXTRACTOR_PROMPT, formatExtractorInput, formatExtractorInputFromDescription, parseExtractorOutput, TaskTagPlugin, extractPerTaskChatId, extractPerTaskChatName, NotifyType, LarkCliTaskNotifyHook, createLarkCliTaskNotifyHook, PerTaskGroupChatPlugin, createPerTaskGroupChatPlugin };
2954
+ export { createAutoTaskPlugin, pickGateConfidence, LayaDecisionPort, loadLayaAuthForPurpose, LAYA_REUSE_THRESHOLD, LayaDecisionPlugin, createLayaDecisionPlugin, WORKFLOW_EXTRACTOR_PROMPT, formatExtractorInput, formatExtractorInputFromDescription, parseExtractorOutput, TaskTagPlugin, extractPerTaskChatId, extractPerTaskChatName, NotifyType, LarkCliTaskNotifyHook, createLarkCliTaskNotifyHook, PerTaskGroupChatPlugin, createPerTaskGroupChatPlugin };
@@ -10,7 +10,7 @@ import {
10
10
  } from "./roy-agent-core-1pg0fepg.js";
11
11
  import {
12
12
  builtInPrompts
13
- } from "./roy-agent-core-vs5gxgwh.js";
13
+ } from "./roy-agent-core-ma35jyd9.js";
14
14
  import {
15
15
  DEFAULT_SUBAGENT_PROMPT
16
16
  } from "./roy-agent-core-07fvy74y.js";
@@ -1832,14 +1832,33 @@ ${reuseSection.markdown}`;
1832
1832
  async forceStopAll() {
1833
1833
  return this.forceStopAllAcrossParents();
1834
1834
  }
1835
- async dispose() {
1835
+ async dispose(options) {
1836
1836
  for (const [, timer] of this.progressTimers) {
1837
1837
  try {
1838
1838
  clearInterval(timer);
1839
1839
  } catch {}
1840
1840
  }
1841
1841
  this.progressTimers.clear();
1842
+ const skipAbortRunningTasks = options?.skipAbortRunningTasks === true;
1843
+ const waitForRunningTasks = options?.waitForRunningTasks === true;
1844
+ if (waitForRunningTasks && skipAbortRunningTasks) {
1845
+ const waitStart = Date.now();
1846
+ const WAIT_TIMEOUT_MS = 10 * 60 * 1000;
1847
+ while (Date.now() - waitStart < WAIT_TIMEOUT_MS) {
1848
+ let running = 0;
1849
+ for (const [, t] of this.tasks) {
1850
+ if (t.status === "running" || t.status === "pending")
1851
+ running++;
1852
+ }
1853
+ if (running === 0)
1854
+ break;
1855
+ logger3.info(`[BackgroundTaskManager] dispose waiting for ${running} running task(s)`);
1856
+ await new Promise((resolve) => setTimeout(resolve, 200));
1857
+ }
1858
+ }
1842
1859
  for (const [, controller] of this.abortControllers) {
1860
+ if (skipAbortRunningTasks)
1861
+ break;
1843
1862
  try {
1844
1863
  controller.abort();
1845
1864
  } catch {}
@@ -2,6 +2,11 @@ import {
2
2
  NEW_TASK_ID_HASH_PLACEHOLDER,
3
3
  NEW_TASK_ID_PLACEHOLDER
4
4
  } from "./roy-agent-core-tr3ep81v.js";
5
+ import {
6
+ TASK_COMPLETE_ITEM_BY_ITEM,
7
+ TASK_COMPLETE_VERIFY_DUTY,
8
+ TASK_COMPLETION_HINT_ANCHOR
9
+ } from "./roy-agent-core-6m4bw8jy.js";
5
10
  import {
6
11
  getEnvContextOrEmpty,
7
12
  init_env_context,
@@ -38,7 +43,7 @@ var CreateTaskToolSchema = z.object({
38
43
  description: z.string().optional().describe("Task description"),
39
44
  priority: z.enum(["low", "medium", "high"]).optional().describe("Task priority"),
40
45
  type: z.enum(["normal", "cycle", "longterm"]).optional().default("normal").describe("Task type. normal: 常规任务,需要跟踪完成. cycle: 周期任务(如每周发周报),无需 complete,定期执行. longterm: 长期任务/愿景目标(如提升公司影响力),难以一次性完成,持续跟进"),
41
- goals_and_expected_deliverables: z.string().optional().describe("Goals and expected deliverables"),
46
+ goals_and_expected_deliverables: z.string().optional().describe("Goals and expected deliverables. " + "⚠️ 写成可逐条验收的编号子项(1. … 2. … 3. …),每条是一个可判定的交付/动作" + "(文件断言:文件存在/大小/内容;进程断言:命令 exit code/stdout;" + "测试断言:pass/fail 计数;状态断言:版本号/HTTP 200/状态字段)。" + "避免一句话模糊目标(如「做好 X」「完成调研」)——" + "任务完成时 completion hook 会按这些子项逐条对照操作记录验收。"),
42
47
  due_date: z.string().optional().describe("Due date (ISO format)"),
43
48
  tags: z.array(z.string()).optional().describe("Task tags (for search, stored in task.tags JSON field)"),
44
49
  project_path: z.string().default("unknown").describe("Project root path for task positioning. " + "Used to locate the project space for this task. " + "Use 'unknown' if unknown. (Defaults to 'unknown'.)"),
@@ -84,7 +89,7 @@ var UpdateTaskToolSchema = withLegacyTaskId({
84
89
  type: z.enum(["normal", "cycle", "longterm"]).optional().describe("Task type"),
85
90
  progress: z.number().min(0).max(100).optional(),
86
91
  current_status: z.string().optional(),
87
- goals_and_expected_deliverables: z.string().optional(),
92
+ goals_and_expected_deliverables: z.string().optional().describe("Goals and expected deliverables. " + "⚠️ 追加/修改时同样写成可逐条验收的编号子项(1. … 2. … 3. …)," + "每条是一个可判定的交付/动作(文件断言:文件存在/大小/内容;" + "进程断言:命令 exit code/stdout;测试断言:pass/fail 计数;" + "状态断言:版本号/HTTP 200/状态字段)。" + "避免一句话模糊目标(如「做好 X」「完成调研」)——" + "任务完成时 completion hook 会按这些子项逐条对照操作记录验收。" + "(Task #4833 — 与 task_create 的 goals 规则同源)"),
88
93
  due_date: z.string().optional(),
89
94
  tags: z.array(z.string()).optional(),
90
95
  project_path: z.string().optional().describe("Project path, identifies which project this task belongs to"),
@@ -316,6 +321,37 @@ function createTaskTool(taskComponent) {
316
321
  - Same-theme tasks cluster under one parent
317
322
  - Long-tail/stranded tasks should be archived or merged
318
323
 
324
+ ## ⚠️ Goals 子项拆分规则 (Task #4791)
325
+
326
+ \`goals_and_expected_deliverables\` **MUST** be written as
327
+ **numbered sub-items** (编号子项) that can each be verified
328
+ one by one (逐条验收) — for \`type=normal\` tasks:
329
+
330
+ 1. 实现 X(含文件路径)
331
+ 2. 测试 Y 覆盖 Z 场景(pass/fail 计数)
332
+ 3. 文档 W 落盘到 <path>
333
+
334
+ ⚠️ For \`type=longterm\` / \`type=cycle\` tasks there is no
335
+ per-item completion acceptance(不走逐条 complete 验收), so the
336
+ MUST above does not apply; for those, it is **strongly recommended**
337
+ (建议) to split goals into staged, individually checkable sub-items
338
+ (阶段性可判子项) instead of one vague sentence.
339
+
340
+ Why: when the task is completed, the completion hook
341
+ (task:after.complete) judges the whole goal against the
342
+ operation-record chain, using each sub-item as fine-grained
343
+ evidence (逐条对照的依据). Vague one-line goals (一句话模糊目标,
344
+ 如「做好 X」「完成调研」) give the judge nothing to check and
345
+ make a completion hint much more likely.
346
+
347
+ Reference the four atomic assertion forms (Task #4768
348
+ 四原子形态) when writing each sub-item:
349
+
350
+ - 文件断言 (file assertion): file exists / size / content grep
351
+ - 进程断言 (process assertion): command exit code / stdout match
352
+ - 测试断言 (test assertion): pass/fail counts, no NEW fail
353
+ - 状态断言 (state assertion): version number / HTTP 200 / status field
354
+
319
355
  ## ⚠️ Spatial + Temporal Context (Task #2325)
320
356
 
321
357
  A task MUST carry spatio-temporal context so it survives task-handoff
@@ -917,8 +953,28 @@ Mark a task as completed (progress=100, status=completed). Creates an operation
917
953
 
918
954
  1. Read the task's \`goal\` (description) and \`delivery\` (goals_and_expected_deliverables) fields.
919
955
  - Use \`task_get(task_id=<id>)\` to fetch the full task record.
920
- 2. **Validate** whether the goal and delivery match what you actually delivered.
921
- - If mismatch, either: (a) extend the task via \`task_update\`, or (b) honestly note the discrepancy in your final report.
956
+ 2. **Validate whether each goal sub-item was achieved** (${TASK_COMPLETE_ITEM_BY_ITEM}).
957
+ - Goals should be numbered sub-items (编号子项, see task_create rules). Check
958
+ each sub-item against the operation-record chain (操作记录链) and the
959
+ deliverable evidence (context.deliverable_storage_path).
960
+ - The completion hook (task:after.complete) judges the whole goal vs.
961
+ deliverables; numbered sub-items give it finer-grained evidence to
962
+ reason with (逐条对照的证据粒度). If it detects that the goal or
963
+ deliverables appear unmet, the task context will carry a
964
+ \`[${TASK_COMPLETION_HINT_ANCHOR}]\`.
965
+ - **If you receive a [${TASK_COMPLETION_HINT_ANCHOR}]**, you MUST verify it
966
+ (${TASK_COMPLETE_VERIFY_DUTY}): re-read the task via \`task_get\`, then do exactly one of:
967
+ (a) 确认未完成 → \`task_update(task_id=<id>, status=todo)\` reopen and continue; or
968
+ (b) 确认已完成 → \`task_operation_create\` (milestone_type=decision,
969
+ milestone_title=<一行结论>) state why the sub-items are actually
970
+ met (说明理由).
971
+ Never ignore it (不允许无视).
972
+ - If the goal and delivery mismatch **without** a hint, either:
973
+ (a) extend the task via \`task_update\`, or
974
+ (b) record the discrepancy via \`task_operation_create\`
975
+ (milestone_type=decision) — the reason **must** land in the
976
+ operation record (必须落到操作记录里), not merely a line in
977
+ your final report (不能只在报告里带一句).
922
978
  3. **Provide \`deliverable_storage_path\`** parameter with the deliverable's address:
923
979
  - Feishu doc URL (e.g., https://my.feishu.cn/docx/abc)
924
980
  - Local file path (e.g., /tmp/report.md)
@@ -68,6 +68,8 @@ import {
68
68
 
69
69
  // src/env/agent/agent-component.ts
70
70
  init_logger();
71
+ import fs from "fs/promises";
72
+ import path from "path";
71
73
  import { z } from "zod";
72
74
 
73
75
  // src/env/task/plugins/max-iteration-reminder-plugin.ts
@@ -600,6 +602,7 @@ class AgentComponent extends BaseComponent {
600
602
  doomLoopCaches = new Map;
601
603
  fallbackState = new Map;
602
604
  configComponent;
605
+ skipAbortRunningTasksOnStop = false;
603
606
  configWatcher;
604
607
  messageConverter = new SessionMessageConverter;
605
608
  _constructorConfig;
@@ -622,6 +625,7 @@ class AgentComponent extends BaseComponent {
622
625
  throw new Error("ConfigComponent is required for AgentComponent initialization");
623
626
  }
624
627
  this.configComponent = options.configComponent;
628
+ this.skipAbortRunningTasksOnStop = options.skipAbortRunningTasksOnStop === true;
625
629
  await this.registerConfig(options);
626
630
  await this.initRegistry(options.configComponent);
627
631
  this.setStatus("running");
@@ -794,13 +798,19 @@ class AgentComponent extends BaseComponent {
794
798
  }
795
799
  async onStop() {
796
800
  logger2.info("[AgentComponent] Stopping and cleaning up resources...");
797
- for (const [name, agent] of this.agents) {
798
- if (agent.status === "running" || agent.status === "idle") {
799
- this.abort(name);
800
- logger2.debug(`[AgentComponent] Aborted agent: ${name}`);
801
+ const skipAbort = this.skipAbortRunningTasksOnStop === true;
802
+ if (!skipAbort) {
803
+ for (const [name, agent] of this.agents) {
804
+ if (agent.status === "running" || agent.status === "idle") {
805
+ this.abort(name);
806
+ logger2.debug(`[AgentComponent] Aborted agent: ${name}`);
807
+ }
801
808
  }
809
+ } else {
810
+ logger2.info("[AgentComponent] skipAbortRunningTasksOnStop=true — keeping running agent runs alive");
802
811
  }
803
812
  this.abortControllers.clear();
813
+ this.aborted.clear();
804
814
  this.doomLoopCaches.clear();
805
815
  if (this.configWatcher) {
806
816
  this.configWatcher();
@@ -1603,12 +1613,18 @@ class AgentComponent extends BaseComponent {
1603
1613
  const session = await sessionComponent.get(sessionId);
1604
1614
  const existingSnapshot = session?.metadata?.cacheSnapshot;
1605
1615
  if (existingSnapshot && typeof existingSnapshot === "object" && existingSnapshot.systemPrompt) {
1606
- logger2.debug(`[AgentComponent._run] Reusing cacheSnapshot for session ${sessionId} (snapshotAt=${existingSnapshot.snapshotAt})`);
1607
- return {
1608
- snapshot: existingSnapshot,
1609
- systemPromptOverride: existingSnapshot.systemPrompt
1610
- };
1616
+ const stale = await this._isMemorySnapshotStale(existingSnapshot);
1617
+ if (stale) {
1618
+ logger2.info(`[AgentComponent._run] cacheSnapshot for session ${sessionId} is stale (${stale}); recreating`);
1619
+ } else {
1620
+ logger2.debug(`[AgentComponent._run] Reusing cacheSnapshot for session ${sessionId} (snapshotAt=${existingSnapshot.snapshotAt})`);
1621
+ return {
1622
+ snapshot: existingSnapshot,
1623
+ systemPromptOverride: existingSnapshot.systemPrompt
1624
+ };
1625
+ }
1611
1626
  }
1627
+ const memoryStat = await this._collectMemoryStat();
1612
1628
  let resolvedSystemPrompt = initialSystemPrompt;
1613
1629
  if (resolvedSystemPrompt && resolvedSystemPrompt.includes("{{memory}}")) {
1614
1630
  try {
@@ -1627,7 +1643,8 @@ class AgentComponent extends BaseComponent {
1627
1643
  const snapshot = {
1628
1644
  systemPrompt: resolvedSystemPrompt ?? agentConfigSystemPrompt ?? "",
1629
1645
  additionInfo: effectiveContext.additionInfo || "",
1630
- snapshotAt: Date.now()
1646
+ snapshotAt: Date.now(),
1647
+ memoryStat
1631
1648
  };
1632
1649
  try {
1633
1650
  await sessionComponent.update(sessionId, {
@@ -1646,6 +1663,46 @@ class AgentComponent extends BaseComponent {
1646
1663
  return { snapshot: null, systemPromptOverride: undefined };
1647
1664
  }
1648
1665
  }
1666
+ async _isMemorySnapshotStale(snapshot) {
1667
+ const stat = snapshot.memoryStat;
1668
+ if (!stat || typeof stat !== "object") {
1669
+ return "no-memoryStat (legacy snapshot)";
1670
+ }
1671
+ const current = await this._collectMemoryStat();
1672
+ if (current.globalMtime !== stat.globalMtime) {
1673
+ return `global memory.md mtime changed (${stat.globalMtime} -> ${current.globalMtime})`;
1674
+ }
1675
+ if (current.projectMtime !== stat.projectMtime) {
1676
+ return `project memory.md mtime changed (${stat.projectMtime} -> ${current.projectMtime})`;
1677
+ }
1678
+ return null;
1679
+ }
1680
+ async _collectMemoryStat() {
1681
+ const empty = { globalMtime: 0, projectMtime: 0 };
1682
+ try {
1683
+ const memoryComponent = this.env?.getComponent?.("memory");
1684
+ if (!memoryComponent?.getMemoryPaths || !memoryComponent.getMemoryFile) {
1685
+ return empty;
1686
+ }
1687
+ const memoryFile = memoryComponent.getMemoryFile();
1688
+ const paths = memoryComponent.getMemoryPaths();
1689
+ const result = { globalMtime: 0, projectMtime: 0 };
1690
+ for (const p of paths) {
1691
+ const filePath = path.join(p.path, memoryFile);
1692
+ try {
1693
+ const st = await fs.stat(filePath);
1694
+ if (p.type === "global") {
1695
+ result.globalMtime = st.mtimeMs;
1696
+ } else if (p.type === "project") {
1697
+ result.projectMtime = st.mtimeMs;
1698
+ }
1699
+ } catch {}
1700
+ }
1701
+ return result;
1702
+ } catch {
1703
+ return empty;
1704
+ }
1705
+ }
1649
1706
  async _fallbackReplaceMemory(systemPrompt) {
1650
1707
  if (!systemPrompt.includes("{{memory}}"))
1651
1708
  return systemPrompt;
@@ -13,7 +13,7 @@ import {
13
13
  createPerTaskGroupChatPlugin,
14
14
  extractPerTaskChatId,
15
15
  extractPerTaskChatName
16
- } from "./roy-agent-core-v8bz7d8x.js";
16
+ } from "./roy-agent-core-b7jbqfsy.js";
17
17
  import {
18
18
  getSharedEventStore
19
19
  } from "./roy-agent-core-na18zwj0.js";
@@ -311,6 +311,88 @@ var SELF_REMINDER_MESSAGE = `[Timer Self-Reminder]
311
311
 
312
312
  请根据分析结果,创建 1-3 个有价值的任务并执行。`;
313
313
  var TIMER_OPTIONS_KEY_GET_MANAGER = "getBackgroundTaskManager";
314
+ var SCHEDULE_TICK_INTERVAL_MS = 30 * 1000;
315
+ var TIMER_OPTIONS_KEY_SCHEDULE = "schedule";
316
+ var TIMER_OPTIONS_KEY_SESSION_ID = "sessionId";
317
+ var TIMER_OPTIONS_KEY_SKIP_WHEN_RUNNING = "skipWhenRunning";
318
+ var TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/;
319
+ function isValidScheduleTime(time) {
320
+ return typeof time === "string" && TIME_PATTERN.test(time);
321
+ }
322
+ function parseSchedule(raw) {
323
+ if (!Array.isArray(raw))
324
+ return [];
325
+ const result = [];
326
+ for (const item of raw) {
327
+ if (!item || typeof item !== "object")
328
+ continue;
329
+ const obj = item;
330
+ if (!isValidScheduleTime(obj.time))
331
+ continue;
332
+ if (typeof obj.prompt !== "string" || obj.prompt.trim().length === 0)
333
+ continue;
334
+ const entry = {
335
+ time: obj.time,
336
+ prompt: obj.prompt
337
+ };
338
+ if (typeof obj.label === "string" && obj.label.length > 0) {
339
+ entry.label = obj.label;
340
+ }
341
+ if (typeof obj.sessionId === "string" && obj.sessionId.length > 0) {
342
+ entry.sessionId = obj.sessionId;
343
+ }
344
+ result.push(entry);
345
+ }
346
+ return result;
347
+ }
348
+ function validateScheduleConfig(config) {
349
+ const errors = [];
350
+ const options = config.options ?? {};
351
+ const raw = options[TIMER_OPTIONS_KEY_SCHEDULE];
352
+ if (raw === undefined || raw === null)
353
+ return errors;
354
+ if (!Array.isArray(raw)) {
355
+ errors.push("Timer schedule must be an array of { time, prompt } entries");
356
+ return errors;
357
+ }
358
+ if (raw.length === 0) {
359
+ errors.push("Timer schedule must not be empty");
360
+ return errors;
361
+ }
362
+ raw.forEach((item, index) => {
363
+ if (!item || typeof item !== "object") {
364
+ errors.push(`Timer schedule[${index}] must be an object`);
365
+ return;
366
+ }
367
+ const obj = item;
368
+ if (!isValidScheduleTime(obj.time)) {
369
+ errors.push(`Timer schedule[${index}].time must be in HH:MM format (24-hour), got: ${String(obj.time)}`);
370
+ }
371
+ if (typeof obj.prompt !== "string" || obj.prompt.trim().length === 0) {
372
+ errors.push(`Timer schedule[${index}].prompt is required and must be a non-empty string`);
373
+ }
374
+ });
375
+ const seenTimes = new Set;
376
+ const duplicatedTimes = new Set;
377
+ for (const item of raw) {
378
+ if (!item || typeof item !== "object")
379
+ continue;
380
+ const time = item.time;
381
+ if (!isValidScheduleTime(time))
382
+ continue;
383
+ if (seenTimes.has(time)) {
384
+ duplicatedTimes.add(time);
385
+ }
386
+ seenTimes.add(time);
387
+ }
388
+ if (duplicatedTimes.size > 0) {
389
+ errors.push(`Timer schedule contains duplicate time entries: ${Array.from(duplicatedTimes).join(", ")}`);
390
+ }
391
+ if (config.interval !== undefined && config.interval !== null) {
392
+ errors.push("Timer schedule and interval are mutually exclusive: use schedule for fixed daily times, or interval for periodic triggering");
393
+ }
394
+ return errors;
395
+ }
314
396
 
315
397
  class TimerInstance {
316
398
  config;
@@ -318,21 +400,38 @@ class TimerInstance {
318
400
  sessionId;
319
401
  getRunningTasks;
320
402
  customPrompt;
403
+ schedule;
404
+ scheduleMode;
405
+ invalidScheduleWarned = false;
406
+ scheduleSessionId;
407
+ skipWhenRunning;
408
+ firedKeys = new Set;
409
+ now;
321
410
  status = "created";
322
411
  timerId;
323
412
  eventHandler;
324
- constructor(config, getRunningTasks) {
413
+ constructor(config, getRunningTasks, now) {
325
414
  this.config = config;
326
- this.interval = typeof config.interval === "number" && config.interval > 0 ? config.interval : DEFAULT_TIMER_INTERVAL_MS;
415
+ this.now = now ?? (() => Date.now());
416
+ const options = config.options ?? {};
417
+ const rawSchedule = options[TIMER_OPTIONS_KEY_SCHEDULE];
418
+ const scheduleConfigured = rawSchedule !== undefined && rawSchedule !== null;
419
+ this.schedule = parseSchedule(rawSchedule);
420
+ this.scheduleMode = scheduleConfigured;
421
+ const isScheduleMode = this.scheduleMode;
422
+ this.interval = isScheduleMode ? SCHEDULE_TICK_INTERVAL_MS : typeof config.interval === "number" && config.interval > 0 ? config.interval : DEFAULT_TIMER_INTERVAL_MS;
327
423
  this.sessionId = `timer-${config.id}-${process.pid}`;
328
- const optionsPrompt = config.options?.prompt;
424
+ const explicitSessionId = options[TIMER_OPTIONS_KEY_SESSION_ID];
425
+ this.scheduleSessionId = typeof explicitSessionId === "string" && explicitSessionId.length > 0 ? explicitSessionId : `timer-${config.id}`;
426
+ this.skipWhenRunning = options[TIMER_OPTIONS_KEY_SKIP_WHEN_RUNNING] === true;
427
+ const optionsPrompt = options.prompt;
329
428
  if (typeof optionsPrompt === "string" && optionsPrompt.trim().length > 0) {
330
429
  this.customPrompt = optionsPrompt;
331
430
  }
332
431
  if (getRunningTasks) {
333
432
  this.getRunningTasks = getRunningTasks;
334
433
  } else {
335
- const factory = config.options?.[TIMER_OPTIONS_KEY_GET_MANAGER];
434
+ const factory = options[TIMER_OPTIONS_KEY_GET_MANAGER];
336
435
  if (typeof factory === "function") {
337
436
  const manager = factory();
338
437
  this.getRunningTasks = () => manager.listTasks();
@@ -341,6 +440,9 @@ class TimerInstance {
341
440
  }
342
441
  }
343
442
  }
443
+ get isScheduleMode() {
444
+ return this.scheduleMode;
445
+ }
344
446
  async start() {
345
447
  if (this.status === "running")
346
448
  return;
@@ -366,6 +468,10 @@ class TimerInstance {
366
468
  this.eventHandler = undefined;
367
469
  }
368
470
  tick() {
471
+ if (this.isScheduleMode) {
472
+ this.tickSchedule();
473
+ return;
474
+ }
369
475
  let allTasks;
370
476
  try {
371
477
  allTasks = this.getRunningTasks();
@@ -380,19 +486,81 @@ class TimerInstance {
380
486
  const event = this.buildSelfReminderEvent();
381
487
  this.eventHandler?.(event);
382
488
  }
383
- buildSelfReminderEvent() {
384
- const timestamp = Date.now();
385
- const message = this.customPrompt ?? SELF_REMINDER_MESSAGE;
489
+ tickSchedule() {
490
+ if (this.schedule.length === 0) {
491
+ if (!this.invalidScheduleWarned) {
492
+ this.invalidScheduleWarned = true;
493
+ console.warn(`[TimerInstance] Timer ${this.config.id} has an invalid or empty schedule configuration; ` + `no events will be fired. Check options.schedule (expected [{ time: "HH:MM", prompt: "..." }]).`);
494
+ }
495
+ return;
496
+ }
497
+ const now = new Date(this.now());
498
+ const hh = String(now.getHours()).padStart(2, "0");
499
+ const mm = String(now.getMinutes()).padStart(2, "0");
500
+ const currentTime = `${hh}:${mm}`;
501
+ const dateKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
502
+ for (const entry of this.schedule) {
503
+ if (entry.time !== currentTime)
504
+ continue;
505
+ const firedKey = `${dateKey}#${entry.time}`;
506
+ if (this.firedKeys.has(firedKey)) {
507
+ continue;
508
+ }
509
+ if (this.skipWhenRunning) {
510
+ let hasRunning = false;
511
+ try {
512
+ const allTasks = this.getRunningTasks();
513
+ hasRunning = Array.isArray(allTasks) && allTasks.some((t) => t.status === "running");
514
+ } catch (error) {
515
+ console.warn(`[TimerInstance] Failed to get running tasks, skipping tick: ${error}`);
516
+ return;
517
+ }
518
+ if (hasRunning) {
519
+ continue;
520
+ }
521
+ }
522
+ this.firedKeys.add(firedKey);
523
+ this.pruneFiredKeys(dateKey);
524
+ const event = this.buildSelfReminderEvent(entry);
525
+ this.eventHandler?.(event);
526
+ return;
527
+ }
528
+ }
529
+ pruneFiredKeys(currentDateKey) {
530
+ if (this.firedKeys.size <= 32)
531
+ return;
532
+ for (const key of this.firedKeys) {
533
+ const datePart = key.split("#")[0];
534
+ if (datePart !== currentDateKey) {
535
+ this.firedKeys.delete(key);
536
+ }
537
+ }
538
+ }
539
+ buildSelfReminderEvent(entry) {
540
+ const timestamp = this.now();
541
+ const message = entry?.prompt ?? this.customPrompt ?? SELF_REMINDER_MESSAGE;
542
+ const triggerSessionId = entry !== undefined ? entry.sessionId ?? this.scheduleSessionId : undefined;
543
+ const metadata = {
544
+ source: "event-source",
545
+ sourceId: this.config.id,
546
+ sourceMessageType: "timer.selfReminder",
547
+ sessionId: this.sessionId
548
+ };
549
+ if (triggerSessionId !== undefined) {
550
+ metadata.trigger_session_id = triggerSessionId;
551
+ metadata.createSessionIfMissing = true;
552
+ }
553
+ if (entry?.label !== undefined) {
554
+ metadata.scheduleLabel = entry.label;
555
+ }
556
+ if (entry !== undefined) {
557
+ metadata.scheduleTime = entry.time;
558
+ }
386
559
  return {
387
560
  id: `es-${this.config.id}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`,
388
561
  type: "event-source.event.timer",
389
562
  timestamp,
390
- metadata: {
391
- source: "event-source",
392
- sourceId: this.config.id,
393
- sourceMessageType: "timer.selfReminder",
394
- sessionId: this.sessionId
395
- },
563
+ metadata,
396
564
  payload: {
397
565
  sourceId: this.config.id,
398
566
  sourceType: "timer",
@@ -415,6 +583,9 @@ class TimerInstance {
415
583
  __legacyDecorateClassTS([
416
584
  TracedAs("timer.tick", { recordParams: false, log: false })
417
585
  ], TimerInstance.prototype, "tick", null);
586
+ __legacyDecorateClassTS([
587
+ TracedAs("timer.tickSchedule", { recordParams: false, log: false })
588
+ ], TimerInstance.prototype, "tickSchedule", null);
418
589
  __legacyDecorateClassTS([
419
590
  TracedAs("timer.buildSelfReminderEvent", { recordParams: false, log: false })
420
591
  ], TimerInstance.prototype, "buildSelfReminderEvent", null);
@@ -423,7 +594,9 @@ var timerHandler = {
423
594
  validateConfig(config) {
424
595
  const errors = [];
425
596
  const { interval } = config;
426
- if (interval !== undefined && interval !== null) {
597
+ errors.push(...validateScheduleConfig(config));
598
+ const hasSchedule = config.options?.[TIMER_OPTIONS_KEY_SCHEDULE] !== undefined;
599
+ if (!hasSchedule && interval !== undefined && interval !== null) {
427
600
  if (typeof interval !== "number" || !Number.isFinite(interval)) {
428
601
  errors.push("Timer interval must be a finite number");
429
602
  } else if (interval <= 0) {
@@ -439,7 +612,7 @@ var timerHandler = {
439
612
  return [
440
613
  {
441
614
  name: "interval",
442
- description: `Timer interval in milliseconds (default: ${DEFAULT_TIMER_INTERVAL_MS} = 10 minutes)`,
615
+ description: `Timer interval in milliseconds (default: ${DEFAULT_TIMER_INTERVAL_MS} = 10 minutes). Mutually exclusive with schedule.`,
443
616
  required: false,
444
617
  type: "number"
445
618
  },
@@ -448,6 +621,18 @@ var timerHandler = {
448
621
  description: "Custom prompt message for SelfReminder events (replaces default message)",
449
622
  required: false,
450
623
  type: "string"
624
+ },
625
+ {
626
+ name: TIMER_OPTIONS_KEY_SCHEDULE,
627
+ description: "JSON array of { time: 'HH:MM', prompt: string, label?: string, sessionId?: string } " + "for fixed daily trigger times (e.g. 10:00 / 17:00 / 20:00). Mutually exclusive with interval.",
628
+ required: false,
629
+ type: "string"
630
+ },
631
+ {
632
+ name: TIMER_OPTIONS_KEY_SESSION_ID,
633
+ description: "Explicit session ID for timer events (schedule mode). Events are routed to this " + "session instead of the main REPL session.",
634
+ required: false,
635
+ type: "string"
451
636
  }
452
637
  ];
453
638
  }
@@ -1,3 +1,9 @@
1
+ import {
2
+ TASK_COMPLETE_ITEM_BY_ITEM,
3
+ TASK_COMPLETE_VERIFY_DUTY,
4
+ TASK_COMPLETION_HINT_ANCHOR
5
+ } from "./roy-agent-core-6m4bw8jy.js";
6
+
1
7
  // src/env/prompt/prompts-index.ts
2
8
  var builtInPrompts = {
3
9
  default: `你是 {{agent_name}} 接待智能体(Receptionist Agent)—— {{agent_name}} 系统的智能入口。
@@ -125,7 +131,7 @@ var builtInPrompts = {
125
131
  - ✅ 功能开发、bug 修复、重构、文档生成
126
132
  - ✅ 网络搜索和研究
127
133
 
128
- **roy** 的提示词中内置了任务生命周期——它知道如何使用 \`task_create\`、\`task_update\`、\`task_operation_create\` 和 \`task_complete\` 来跟踪每项工作。
134
+ **roy** 的提示词中内置了任务生命周期——它知道如何使用 \`task_create\`、\`task_update\`、\`task_operation_create\` 和 \`task_complete\` 来跟踪每项工作。task_complete 时会${TASK_COMPLETE_VERIFY_DUTY}:按子项逐条验收(${TASK_COMPLETE_ITEM_BY_ITEM}),若收到 [${TASK_COMPLETION_HINT_ANCHOR}] 必须核实并二选一(重开 / 落 decision 说明理由)。
129
135
 
130
136
  推荐使用:
131
137
  \`\`\`
@@ -893,8 +899,13 @@ Follow this lifecycle for every task:
893
899
  ┌─────────────────────────────────────────────────────────────────┐
894
900
  │ 6. COMPLETE (task_complete) │
895
901
  │ - Mark task as completed (progress=100%, status=completed) │
896
- │ - Verify all goals_and_expected_deliverables are met │
897
- │ - Create completion operation record │
902
+ │ - ${TASK_COMPLETE_ITEM_BY_ITEM}: check each goal sub-item │
903
+ │ against the operation-record chain + deliverable evidence │
904
+ │ - If a [${TASK_COMPLETION_HINT_ANCHOR}] appears in task │
905
+ │ context, you MUST ${TASK_COMPLETE_VERIFY_DUTY}: reopen │
906
+ │ (task_update status=todo) or record why it is met │
907
+ │ (task_operation_create decision). Never ignore it. │
908
+ │ - Create completion operation record │
898
909
  └─────────────────────────────────────────────────────────────────┘
899
910
  \`\`\`
900
911