@bizagent/dsh 0.0.0-stage → 1.0.0

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 (181) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/LICENSE +21 -0
  3. package/README.md +61 -2
  4. package/cordis.patch.yml +14 -0
  5. package/docs/README.md +23 -0
  6. package/docs/architecture.md +154 -0
  7. package/docs/concepts.md +192 -0
  8. package/docs/configuration.md +154 -0
  9. package/docs/quickstart.md +103 -0
  10. package/docs/quickstart.zh-CN.md +86 -0
  11. package/docs/tools.md +607 -0
  12. package/docs/user-guide.zh-CN.md +132 -0
  13. package/docs/verification.md +69 -0
  14. package/examples/README.md +64 -0
  15. package/examples/demo-runtime.mjs +71 -0
  16. package/examples/learning-lab.mjs +263 -0
  17. package/examples/scenarios.mjs +45 -0
  18. package/lib/agent-preset.d.ts +20 -0
  19. package/lib/agent-preset.js +46 -0
  20. package/lib/agent-session-address.d.ts +14 -0
  21. package/lib/agent-session-address.js +15 -0
  22. package/lib/agent-session-coordinator.d.ts +24 -0
  23. package/lib/agent-session-coordinator.js +101 -0
  24. package/lib/cli.d.ts +3 -0
  25. package/lib/cli.js +142 -0
  26. package/lib/client-types/agent-session-address.d.ts +13 -0
  27. package/lib/client-types/client/agent-mentions.d.ts +3 -0
  28. package/lib/client-types/client/api.d.ts +180 -0
  29. package/lib/client-types/client/components.d.ts +22 -0
  30. package/lib/client-types/client/controller.d.ts +23 -0
  31. package/lib/client-types/client/frame-participants.d.ts +5 -0
  32. package/lib/client-types/client/home-name.d.ts +9 -0
  33. package/lib/client-types/client/human-inbox.d.ts +8 -0
  34. package/lib/client-types/client/index.d.ts +13 -0
  35. package/lib/client-types/client/locales.d.ts +796 -0
  36. package/lib/client-types/client/markdown.d.ts +12 -0
  37. package/lib/client-types/client/project-artifact-link.d.ts +10 -0
  38. package/lib/client-types/client/project-outcome-review-inbox.d.ts +13 -0
  39. package/lib/client-types/client/projects.d.ts +11 -0
  40. package/lib/client-types/client/sample-guide.d.ts +9 -0
  41. package/lib/client-types/client/session-resource.d.ts +34 -0
  42. package/lib/client-types/client/session-surfaces.d.ts +24 -0
  43. package/lib/client-types/client/styles.d.ts +1 -0
  44. package/lib/client-types/config.d.ts +55 -0
  45. package/lib/client-types/cron.d.ts +8 -0
  46. package/lib/client-types/domain.d.ts +818 -0
  47. package/lib/client-types/frame-reference.d.ts +4 -0
  48. package/lib/client-types/home-directory.d.ts +5 -0
  49. package/lib/client-types/home-space.d.ts +49 -0
  50. package/lib/client-types/home-store.d.ts +125 -0
  51. package/lib/client-types/inbox-stalled-topic.d.ts +15 -0
  52. package/lib/client-types/inbox-wake-policy.d.ts +18 -0
  53. package/lib/client-types/known-format.d.ts +2 -0
  54. package/lib/client-types/learning-scheduler.d.ts +36 -0
  55. package/lib/client-types/learning-session-runner.d.ts +49 -0
  56. package/lib/client-types/learning-trigger.d.ts +17 -0
  57. package/lib/client-types/learning-usage.d.ts +17 -0
  58. package/lib/client-types/ledger.d.ts +190 -0
  59. package/lib/client-types/local-owner.d.ts +7 -0
  60. package/lib/client-types/message-source.d.ts +9 -0
  61. package/lib/client-types/organization-location.d.ts +19 -0
  62. package/lib/client-types/project-ledger.d.ts +182 -0
  63. package/lib/client-types/project-seat-context.d.ts +17 -0
  64. package/lib/client-types/safe-file.d.ts +23 -0
  65. package/lib/client-types/sample-organization.d.ts +9 -0
  66. package/lib/client-types/service.d.ts +596 -0
  67. package/lib/client-types/shared-knowledge.d.ts +116 -0
  68. package/lib/client-types/ui-contract.d.ts +276 -0
  69. package/lib/client-types/untrusted-text.d.ts +15 -0
  70. package/lib/client-types/work-ledger.d.ts +108 -0
  71. package/lib/client-types/work-output-eval.d.ts +15 -0
  72. package/lib/client-types/worklog-ledger.d.ts +88 -0
  73. package/lib/client-types/worklog-snapshot.d.ts +33 -0
  74. package/lib/client.js +5463 -0
  75. package/lib/config.d.ts +56 -0
  76. package/lib/config.js +115 -0
  77. package/lib/cron.d.ts +9 -0
  78. package/lib/cron.js +127 -0
  79. package/lib/domain.d.ts +819 -0
  80. package/lib/domain.js +662 -0
  81. package/lib/frame-admission.d.ts +6 -0
  82. package/lib/frame-admission.js +39 -0
  83. package/lib/frame-reference.d.ts +5 -0
  84. package/lib/frame-reference.js +12 -0
  85. package/lib/home-archive.d.ts +17 -0
  86. package/lib/home-archive.js +177 -0
  87. package/lib/home-directory.d.ts +6 -0
  88. package/lib/home-directory.js +38 -0
  89. package/lib/home-space.d.ts +50 -0
  90. package/lib/home-space.js +120 -0
  91. package/lib/home-store.d.ts +126 -0
  92. package/lib/home-store.js +837 -0
  93. package/lib/host.d.ts +22 -0
  94. package/lib/host.js +15 -0
  95. package/lib/human-inbox-transport.d.ts +23 -0
  96. package/lib/human-inbox-transport.js +105 -0
  97. package/lib/human-notification-runner.d.ts +83 -0
  98. package/lib/human-notification-runner.js +138 -0
  99. package/lib/human-notification-webhook.d.ts +18 -0
  100. package/lib/human-notification-webhook.js +102 -0
  101. package/lib/human-turn-completion.d.ts +61 -0
  102. package/lib/human-turn-completion.js +157 -0
  103. package/lib/inbox-return-notifier.d.ts +46 -0
  104. package/lib/inbox-return-notifier.js +279 -0
  105. package/lib/inbox-stalled-escalation-runner.d.ts +31 -0
  106. package/lib/inbox-stalled-escalation-runner.js +68 -0
  107. package/lib/inbox-stalled-topic.d.ts +16 -0
  108. package/lib/inbox-stalled-topic.js +44 -0
  109. package/lib/inbox-wake-policy.d.ts +19 -0
  110. package/lib/inbox-wake-policy.js +28 -0
  111. package/lib/inbox-wake-runner.d.ts +45 -0
  112. package/lib/inbox-wake-runner.js +481 -0
  113. package/lib/index.d.ts +29 -0
  114. package/lib/index.js +480 -0
  115. package/lib/known-format.d.ts +3 -0
  116. package/lib/known-format.js +8 -0
  117. package/lib/learning-scheduler.d.ts +37 -0
  118. package/lib/learning-scheduler.js +182 -0
  119. package/lib/learning-session-runner.d.ts +50 -0
  120. package/lib/learning-session-runner.js +435 -0
  121. package/lib/learning-trigger.d.ts +18 -0
  122. package/lib/learning-trigger.js +92 -0
  123. package/lib/learning-usage.d.ts +18 -0
  124. package/lib/learning-usage.js +49 -0
  125. package/lib/ledger.d.ts +191 -0
  126. package/lib/ledger.js +752 -0
  127. package/lib/local-human-host.d.ts +11 -0
  128. package/lib/local-human-host.js +191 -0
  129. package/lib/local-human-token.d.ts +9 -0
  130. package/lib/local-human-token.js +32 -0
  131. package/lib/local-owner.d.ts +8 -0
  132. package/lib/local-owner.js +26 -0
  133. package/lib/message-source.d.ts +10 -0
  134. package/lib/message-source.js +2 -0
  135. package/lib/organization-location.d.ts +20 -0
  136. package/lib/organization-location.js +80 -0
  137. package/lib/organization-workspace.d.ts +22 -0
  138. package/lib/organization-workspace.js +32 -0
  139. package/lib/project-ledger.d.ts +183 -0
  140. package/lib/project-ledger.js +189 -0
  141. package/lib/project-review-transport.d.ts +23 -0
  142. package/lib/project-review-transport.js +155 -0
  143. package/lib/project-seat-context.d.ts +18 -0
  144. package/lib/project-seat-context.js +34 -0
  145. package/lib/proposal-review-transport.d.ts +26 -0
  146. package/lib/proposal-review-transport.js +104 -0
  147. package/lib/route-guard.d.ts +31 -0
  148. package/lib/route-guard.js +80 -0
  149. package/lib/safe-file.d.ts +24 -0
  150. package/lib/safe-file.js +93 -0
  151. package/lib/sample-organization.d.ts +10 -0
  152. package/lib/sample-organization.js +46 -0
  153. package/lib/service.d.ts +597 -0
  154. package/lib/service.js +4076 -0
  155. package/lib/session-surface.d.ts +11 -0
  156. package/lib/session-surface.js +109 -0
  157. package/lib/shared-knowledge-review-transport.d.ts +28 -0
  158. package/lib/shared-knowledge-review-transport.js +85 -0
  159. package/lib/shared-knowledge.d.ts +117 -0
  160. package/lib/shared-knowledge.js +240 -0
  161. package/lib/tools.d.ts +7 -0
  162. package/lib/tools.js +955 -0
  163. package/lib/ui-contract.d.ts +277 -0
  164. package/lib/ui-contract.js +12 -0
  165. package/lib/ui-host.d.ts +24 -0
  166. package/lib/ui-host.js +471 -0
  167. package/lib/untrusted-text.d.ts +16 -0
  168. package/lib/untrusted-text.js +50 -0
  169. package/lib/work-ledger.d.ts +109 -0
  170. package/lib/work-ledger.js +107 -0
  171. package/lib/work-output-eval.d.ts +16 -0
  172. package/lib/work-output-eval.js +58 -0
  173. package/lib/work-runner.d.ts +47 -0
  174. package/lib/work-runner.js +352 -0
  175. package/lib/worklog-ledger.d.ts +89 -0
  176. package/lib/worklog-ledger.js +91 -0
  177. package/lib/worklog-snapshot.d.ts +34 -0
  178. package/lib/worklog-snapshot.js +100 -0
  179. package/lib/writer-guard.d.ts +11 -0
  180. package/lib/writer-guard.js +207 -0
  181. package/package.json +172 -4
@@ -0,0 +1,182 @@
1
+ import { nowIso, parseHomeAddress, stableId } from './domain.js';
2
+ import { nextCronAt, zonedDay } from './cron.js';
3
+ const DAY_MS = 24 * 60 * 60_000;
4
+ /** Single-host, opt-in learning clock. Each due period is consumed at most once by its durable cursor. */
5
+ export class LearningScheduler {
6
+ service;
7
+ runner;
8
+ config;
9
+ clock;
10
+ pass;
11
+ constructor(service, runner, config, clock = Date.now) {
12
+ this.service = service;
13
+ this.runner = runner;
14
+ this.config = config;
15
+ this.clock = clock;
16
+ }
17
+ status(homeInput) {
18
+ const homeAddress = parseHomeAddress(homeInput);
19
+ this.service.store.getHome(homeAddress);
20
+ const base = this.config.learningSchedules.find(item => item.home === homeAddress);
21
+ const policy = this.service.ledger.getLearningSchedulePolicy(homeAddress);
22
+ const schedule = base === undefined ? undefined : effectiveSchedule(base, policy);
23
+ const fingerprint = schedule === undefined ? undefined : stableId('learning-schedule', JSON.stringify({ schedule, revision: policy?.revision ?? 0 }));
24
+ const recordedCursor = this.service.ledger.getLearningScheduleCursor(homeAddress);
25
+ const cursor = recordedCursor?.scheduleFingerprint === fingerprint ? recordedCursor : undefined;
26
+ const active = this.config.learningSchedulerEnabled && schedule !== undefined && policy?.enabled !== false;
27
+ return { homeAddress, hostEnabled: this.config.learningSchedulerEnabled,
28
+ configured: base !== undefined, timeZone: this.config.timeZone,
29
+ ...(base ? { maxDailyRuns: base.dailyRuns } : {}),
30
+ ...(schedule ? { schedule } : {}),
31
+ ...(policy ? { policy } : {}),
32
+ ...(cursor ? { cursor } : {}),
33
+ ...(active ? { nextDueAt: cursor?.nextDueAt ?? nextCronAt(schedule.cron, this.clock(), this.config.timeZone) } : {}),
34
+ runsToday: this.runsToday(homeAddress, this.clock()),
35
+ recentLaunches: this.service.ledger.listLearningLaunches(homeAddress).slice(0, 10),
36
+ };
37
+ }
38
+ async configure(homeInput, input) {
39
+ const homeAddress = parseHomeAddress(homeInput);
40
+ const base = this.config.learningSchedules.find(item => item.home === homeAddress);
41
+ if (!this.config.learningSchedulerEnabled || !base) {
42
+ throw new Error('learning scheduler is not enabled for this Home by its host');
43
+ }
44
+ if (this.service.store.getHome(homeAddress).status !== 'active') {
45
+ throw new Error('learning schedule Home is not active');
46
+ }
47
+ const cron = input.cron.trim();
48
+ if (cron.length > 100)
49
+ throw new Error('learning schedule cron is too long');
50
+ nextCronAt(cron, this.clock(), this.config.timeZone);
51
+ if (!Number.isSafeInteger(input.dailyRuns) || input.dailyRuns < 1 || input.dailyRuns > base.dailyRuns) {
52
+ throw new Error(`learning schedule dailyRuns must be between 1 and ${base.dailyRuns}`);
53
+ }
54
+ if (typeof input.enabled !== 'boolean' || !Number.isSafeInteger(input.expectedRevision)
55
+ || input.expectedRevision < 0)
56
+ throw new Error('invalid learning schedule revision or enabled flag');
57
+ await this.service.ledger.withLearningBatchWrite(async () => {
58
+ const prior = this.service.ledger.getLearningSchedulePolicy(homeAddress);
59
+ if ((prior?.revision ?? 0) !== input.expectedRevision) {
60
+ throw new Error('learning schedule changed; refresh before saving');
61
+ }
62
+ if (prior?.enabled === input.enabled && prior.cron === cron && prior.dailyRuns === input.dailyRuns)
63
+ return;
64
+ await this.service.ledger.putLearningSchedulePolicy({
65
+ schemaVersion: 1, homeAddress, revision: input.expectedRevision + 1,
66
+ enabled: input.enabled, cron, dailyRuns: input.dailyRuns,
67
+ updatedBy: 'local-ui', updatedAt: nowIso(),
68
+ });
69
+ });
70
+ return this.status(homeAddress);
71
+ }
72
+ runDue() {
73
+ if (!this.config.learningSchedulerEnabled)
74
+ return Promise.resolve([]);
75
+ if (this.pass)
76
+ return this.pass;
77
+ const task = this.tick().finally(() => { if (this.pass === task)
78
+ this.pass = undefined; });
79
+ this.pass = task;
80
+ return task;
81
+ }
82
+ async tick() {
83
+ const results = [];
84
+ let launched = 0;
85
+ for (const base of this.config.learningSchedules) {
86
+ const policy = this.service.ledger.getLearningSchedulePolicy(base.home);
87
+ if (policy?.enabled === false)
88
+ continue;
89
+ const schedule = effectiveSchedule(base, policy);
90
+ const now = this.clock();
91
+ const fingerprint = stableId('learning-schedule', JSON.stringify({ schedule,
92
+ revision: policy?.revision ?? 0 }));
93
+ let cursor = this.service.ledger.getLearningScheduleCursor(schedule.home);
94
+ if (cursor?.scheduleFingerprint !== fingerprint) {
95
+ cursor = await this.service.ledger.putLearningScheduleCursor({
96
+ schemaVersion: 1, homeAddress: schedule.home, scheduleFingerprint: fingerprint,
97
+ nextDueAt: nextCronAt(schedule.cron, now, this.config.timeZone), updatedAt: nowIso(),
98
+ });
99
+ }
100
+ if (cursor.nextDueAt > now || launched >= 2) {
101
+ results.push(cursor);
102
+ continue;
103
+ }
104
+ const dueAt = cursor.nextDueAt;
105
+ let outcome;
106
+ let launchId;
107
+ let reason;
108
+ // A launch that has needed review for over a day must not defer every later period.
109
+ for (const id of await this.runner.retireStaleReviews(schedule.home, now, DAY_MS)) {
110
+ this.service.recordRuntimeError(`learning launch ${id}`, new Error('needed review for over 24 hours and was abandoned automatically'), schedule.home);
111
+ }
112
+ const existing = this.service.ledger.getLearningLaunch(scheduledLaunchId(schedule.home, fingerprint, dueAt));
113
+ if (existing) {
114
+ outcome = 'launched';
115
+ launchId = existing.id;
116
+ reason = 'reconciled an already reserved learning launch for this due period';
117
+ }
118
+ else if (now - dueAt > DAY_MS) {
119
+ outcome = 'expired';
120
+ reason = 'missed due period was more than 24 hours old';
121
+ }
122
+ else if (this.runsToday(schedule.home, now) >= schedule.dailyRuns) {
123
+ outcome = 'limit';
124
+ reason = 'daily automatic learning run limit reached';
125
+ }
126
+ else if (this.service.ledger.listLearningLaunches(schedule.home)
127
+ .some(item => ['reserved', 'active', 'needs_review'].includes(item.status))
128
+ || this.service.ledger.listLearningBatches(schedule.home).some(item => item.status === 'open')) {
129
+ outcome = 'deferred';
130
+ reason = 'Home has an unresolved learning launch or open batch';
131
+ }
132
+ else if (!await this.service.hasPendingLearningMaterials(schedule.home)) {
133
+ outcome = 'empty';
134
+ reason = 'no pending material in the bounded learning scan';
135
+ }
136
+ else {
137
+ try {
138
+ const result = await this.runner.requestScheduled(schedule, dueAt, fingerprint, now, policy?.revision ?? 0);
139
+ const launch = result.launch;
140
+ launchId = launch.id;
141
+ outcome = 'launched';
142
+ launched += 1;
143
+ }
144
+ catch (error) {
145
+ launchId = scheduledLaunchId(schedule.home, fingerprint, dueAt);
146
+ if (!this.service.ledger.getLearningLaunch(launchId))
147
+ launchId = undefined;
148
+ outcome = launchId ? 'launched' : 'deferred';
149
+ reason = error instanceof Error ? error.message : String(error);
150
+ this.service.recordRuntimeError(`learning schedule ${schedule.home}`, error);
151
+ if (launchId)
152
+ launched += 1;
153
+ }
154
+ }
155
+ cursor = await this.service.ledger.putLearningScheduleCursor({
156
+ schemaVersion: 1, homeAddress: schedule.home, scheduleFingerprint: fingerprint,
157
+ nextDueAt: nextCronAt(schedule.cron, Math.max(now, dueAt), this.config.timeZone),
158
+ lastDueAt: dueAt, lastOutcome: outcome,
159
+ ...(launchId ? { lastLaunchId: launchId } : {}),
160
+ ...(reason ? { lastReason: reason.slice(0, 2000) } : {}),
161
+ updatedAt: nowIso(),
162
+ });
163
+ results.push(cursor);
164
+ }
165
+ return results;
166
+ }
167
+ runsToday(home, now) {
168
+ const day = zonedDay(now, this.config.timeZone);
169
+ return this.service.ledger.listLearningLaunches(home)
170
+ .filter(item => item.trigger === 'schedule'
171
+ && zonedDay(Date.parse(item.createdAt), this.config.timeZone) === day).length;
172
+ }
173
+ }
174
+ function effectiveSchedule(base, policy) {
175
+ return policy === undefined ? base
176
+ : { ...base, cron: policy.cron, dailyRuns: policy.dailyRuns };
177
+ }
178
+ function scheduledLaunchId(home, fingerprint, dueAt) {
179
+ const period = stableId('learning-period', `${fingerprint}:${dueAt}`);
180
+ return stableId('learning-launch', `${home}:${period}`);
181
+ }
182
+ //# sourceMappingURL=learning-scheduler.js.map
@@ -0,0 +1,50 @@
1
+ import type { Agent, AgentRegistry } from '@deepseek-ai/dsh-agent';
2
+ import type { SessionQueryEngine } from '@deepseek-ai/dsh-session-query';
3
+ import type { ResolvedConfig } from './config.js';
4
+ import type { ResolvedLearningSchedule } from './config.js';
5
+ import { type HomeAddress } from './domain.js';
6
+ import type { BizAgentService } from './service.js';
7
+ type AgentFactory = Pick<AgentRegistry, 'get' | 'create' | 'resume'>;
8
+ type SessionCatalog = Pick<SessionQueryEngine, 'listSessions' | 'readSession'>;
9
+ /** Explicit, Home-bound launcher for dedicated learning Sessions. Unknown creation is never retried blindly. */
10
+ export declare class LearningSessionRunner {
11
+ private readonly agents;
12
+ private readonly sessions;
13
+ private readonly service;
14
+ private readonly config;
15
+ private readonly handles;
16
+ private readonly activating;
17
+ private readonly watching;
18
+ private readonly recoveryRetryAfter;
19
+ private closing;
20
+ constructor(agents: AgentFactory, sessions: SessionCatalog, service: BizAgentService, config: ResolvedConfig);
21
+ request(agent: Agent, idempotencyKey: string, model?: string): Promise<Record<string, unknown>>;
22
+ /** Trusted host scheduler entrypoint. The occurrence is part of the persistent launch identity. */
23
+ requestScheduled(schedule: ResolvedLearningSchedule, dueAt: number, scheduleFingerprint: string, requestedAt?: number, policyRevision?: number): Promise<Record<string, unknown>>;
24
+ private requestFor;
25
+ read(agent: Agent, id: string): Promise<Record<string, unknown>>;
26
+ abandon(agent: Agent, id: string, reason: string): Promise<Record<string, unknown>>;
27
+ /**
28
+ * Abandon launches that have needed review for longer than `maxAgeMs`, so one unresolved launch
29
+ * cannot block every later scheduled learning period. A live worker or an open batch keeps it.
30
+ */
31
+ retireStaleReviews(homeAddress: HomeAddress, now: number, maxAgeMs: number): Promise<string[]>;
32
+ /** On restart, only an already visible worker Session may be resumed. */
33
+ recoverBatch(limit?: number): Promise<{
34
+ recovered: string[];
35
+ needsReview: string[];
36
+ failed: Array<{
37
+ id: string;
38
+ error: unknown;
39
+ }>;
40
+ }>;
41
+ close(): Promise<void>;
42
+ private activateOnce;
43
+ private activate;
44
+ private watch;
45
+ private releaseWhenIdle;
46
+ private markNeedsReview;
47
+ private view;
48
+ }
49
+ export {};
50
+ //# sourceMappingURL=learning-session-runner.d.ts.map
@@ -0,0 +1,435 @@
1
+ import { realpath, stat } from 'node:fs/promises';
2
+ import { freezeMessage } from '@deepseek-ai/dsh-llm';
3
+ import { nowIso, stableId } from './domain.js';
4
+ import { summarizeLearningUsage } from './learning-usage.js';
5
+ const RECOVERY_MAX = 4;
6
+ const RETRY_DELAY_MS = 5 * 60_000;
7
+ /** Explicit, Home-bound launcher for dedicated learning Sessions. Unknown creation is never retried blindly. */
8
+ export class LearningSessionRunner {
9
+ agents;
10
+ sessions;
11
+ service;
12
+ config;
13
+ handles = new Map();
14
+ activating = new Map();
15
+ watching = new Set();
16
+ recoveryRetryAfter = new Map();
17
+ closing = false;
18
+ constructor(agents, sessions, service, config) {
19
+ this.agents = agents;
20
+ this.sessions = sessions;
21
+ this.service = service;
22
+ this.config = config;
23
+ }
24
+ async request(agent, idempotencyKey, model) {
25
+ const selectedModel = model?.trim() || agent.options?.model;
26
+ const selectedProvider = agent.options?.provider;
27
+ const selectedMaxTokens = agent.options?.maxTokens;
28
+ const binding = this.service.bindSession(String(agent.id), agent.session.header.cwd);
29
+ if (!binding)
30
+ throw new Error('learning launch requires a bound Agent Home');
31
+ await this.service.ledger.putBinding(binding);
32
+ return this.requestFor(binding.homeAddress, String(agent.id), idempotencyKey, { ...(selectedProvider ? { provider: selectedProvider } : {}),
33
+ ...(selectedModel ? { model: selectedModel } : {}),
34
+ ...(selectedMaxTokens ? { maxTokens: selectedMaxTokens } : {}) }, 'explicit');
35
+ }
36
+ /** Trusted host scheduler entrypoint. The occurrence is part of the persistent launch identity. */
37
+ async requestScheduled(schedule, dueAt, scheduleFingerprint, requestedAt = Date.now(), policyRevision = 0) {
38
+ if (!Number.isSafeInteger(dueAt) || dueAt < 0)
39
+ throw new Error('invalid learning schedule due time');
40
+ if (!Number.isSafeInteger(requestedAt) || requestedAt < dueAt)
41
+ throw new Error('learning schedule is not due');
42
+ const base = this.config.learningSchedules?.find(item => item.home === schedule.home);
43
+ const policy = this.service.ledger.getLearningSchedulePolicy(schedule.home);
44
+ const expected = base === undefined ? undefined : policy === undefined ? base
45
+ : { ...base, cron: policy.cron, dailyRuns: policy.dailyRuns };
46
+ if (!this.config.learningSchedulerEnabled || policy?.enabled === false
47
+ || (policy?.revision ?? 0) !== policyRevision
48
+ || expected === undefined || JSON.stringify(expected) !== JSON.stringify(schedule)
49
+ || scheduleFingerprint !== stableId('learning-schedule', JSON.stringify({ schedule,
50
+ revision: policyRevision }))) {
51
+ throw new Error('learning schedule is not configured for this host');
52
+ }
53
+ return this.requestFor(schedule.home, `engine:learning-scheduler:${schedule.home}`, stableId('learning-period', `${scheduleFingerprint}:${dueAt}`), { ...(schedule.provider ? { provider: schedule.provider } : {}), model: schedule.model,
54
+ ...(schedule.maxTokens ? { maxTokens: schedule.maxTokens } : {}) }, 'schedule', dueAt, requestedAt);
55
+ }
56
+ async requestFor(homeAddress, requestedBySessionId, idempotencyKey, route, trigger, scheduledFor, requestedAt) {
57
+ if (this.closing)
58
+ throw new Error('learning Session runner is closing');
59
+ const key = idempotencyKey.trim();
60
+ if (!key || key.length > 128)
61
+ throw new Error('learning launch idempotency key must contain 1 to 128 characters');
62
+ if (route.model && route.model.length > 200)
63
+ throw new Error('learning launch model is too long');
64
+ if (this.service.store.getHome(homeAddress).status !== 'active')
65
+ throw new Error('learning launch Home is not active');
66
+ const mapping = this.config.workspaceHomes.find(item => item.address === homeAddress);
67
+ if (!mapping)
68
+ throw new Error(`no DSH Workspace is mapped to learning Home ${homeAddress}`);
69
+ await realDirectory(mapping.root);
70
+ const id = stableId('learning-launch', `${homeAddress}:${key}`);
71
+ const { launch, created } = await this.service.ledger.withLearningBatchWrite(async () => {
72
+ const existing = this.service.ledger.getLearningLaunch(id);
73
+ if (existing) {
74
+ if (existing.requestedBySessionId !== requestedBySessionId || existing.model !== route.model
75
+ || existing.provider !== route.provider || existing.maxTokens !== route.maxTokens
76
+ || (existing.trigger ?? 'explicit') !== trigger || existing.scheduledFor !== scheduledFor) {
77
+ throw new Error('learning launch idempotency key belongs to another request');
78
+ }
79
+ return { launch: existing, created: false };
80
+ }
81
+ if (this.service.ledger.listLearningLaunches(homeAddress)
82
+ .some(item => ['reserved', 'active', 'needs_review'].includes(item.status))) {
83
+ throw new Error('current Home has an unresolved learning Session launch');
84
+ }
85
+ if (this.service.ledger.listLearningBatches(homeAddress).some(run => run.status === 'open')) {
86
+ throw new Error('current Home already has an open learning batch');
87
+ }
88
+ const now = requestedAt === undefined ? nowIso() : new Date(requestedAt).toISOString();
89
+ const launch = await this.service.ledger.putLearningLaunch({
90
+ schemaVersion: 1, id, homeAddress,
91
+ requestedBySessionId,
92
+ workerSessionId: stableId('learning-session', id),
93
+ workspace: mapping.root, promptMessageId: stableId('learning-prompt', id),
94
+ ...(route.provider ? { provider: route.provider } : {}),
95
+ ...(route.model ? { model: route.model } : {}),
96
+ ...(route.maxTokens ? { maxTokens: route.maxTokens } : {}),
97
+ trigger,
98
+ ...(scheduledFor !== undefined ? { scheduledFor } : {}),
99
+ status: 'reserved', createdAt: now, updatedAt: now,
100
+ });
101
+ return { launch, created: true };
102
+ });
103
+ if (!created)
104
+ return launch.status === 'reserved'
105
+ ? this.activateOnce(launch, false) : this.view(launch);
106
+ try {
107
+ return await this.activateOnce(launch, true);
108
+ }
109
+ catch (error) {
110
+ try {
111
+ const live = this.agents.get(launch.workerSessionId);
112
+ const record = live ? undefined : (await this.sessions.listSessions())
113
+ .find(item => String(item.header.id) === launch.workerSessionId);
114
+ if (!live && !record) {
115
+ await this.markNeedsReview(launch, `factory failed and no worker Session was found: ${message(error)}`);
116
+ }
117
+ }
118
+ catch { /* An unavailable catalog leaves the reservation uncertain. */ }
119
+ throw new Error(`learning launch ${id} has an uncertain start; inspect its status: ${message(error)}`, { cause: error });
120
+ }
121
+ }
122
+ async read(agent, id) {
123
+ const binding = this.service.bindSession(String(agent.id), agent.session.header.cwd);
124
+ if (!binding)
125
+ throw new Error('learning launch read requires a bound Agent Home');
126
+ await this.service.ledger.putBinding(binding);
127
+ const launch = this.service.ledger.getLearningLaunch(id);
128
+ if (launch?.homeAddress !== binding.homeAddress)
129
+ throw new Error('learning launch is not in current Home');
130
+ const view = this.view(launch);
131
+ const workerBinding = this.service.ledger.getBinding(launch.workerSessionId);
132
+ if (workerBinding?.homeAddress !== launch.homeAddress || workerBinding.cwd !== launch.workspace) {
133
+ return { ...view, usage: { status: 'unavailable', reason: 'worker Session is not bound to this Home' } };
134
+ }
135
+ try {
136
+ const snapshot = await this.sessions.readSession(launch.workerSessionId);
137
+ if (String(snapshot.session.id) !== launch.workerSessionId || snapshot.session.cwd !== launch.workspace) {
138
+ return { ...view, usage: { status: 'unavailable', reason: 'worker Session identity or Workspace mismatch' } };
139
+ }
140
+ return { ...view, usage: summarizeLearningUsage(snapshot.events) };
141
+ }
142
+ catch {
143
+ return { ...view, usage: { status: 'unavailable', reason: 'worker Session is not readable' } };
144
+ }
145
+ }
146
+ async abandon(agent, id, reason) {
147
+ const binding = this.service.bindSession(String(agent.id), agent.session.header.cwd);
148
+ if (!binding)
149
+ throw new Error('learning launch resolution requires a bound Agent Home');
150
+ await this.service.ledger.putBinding(binding);
151
+ const explanation = reason.trim();
152
+ if (!explanation || explanation.length > 2000)
153
+ throw new Error('learning launch abandonment needs a reason');
154
+ return this.service.ledger.withLearningBatchWrite(async () => {
155
+ const launch = this.service.ledger.getLearningLaunch(id);
156
+ if (launch?.homeAddress !== binding.homeAddress)
157
+ throw new Error('learning launch is not in current Home');
158
+ if (launch.status === 'abandoned')
159
+ return this.view(launch);
160
+ if (launch.status !== 'needs_review')
161
+ throw new Error('only a launch needing review may be abandoned');
162
+ if (this.agents.get(launch.workerSessionId)) {
163
+ throw new Error('learning worker Session is live; inspect it before abandoning the launch');
164
+ }
165
+ if (this.service.ledger.listLearningBatches(binding.homeAddress)
166
+ .some(run => run.reviewerSessionId === launch.workerSessionId && run.status === 'open')) {
167
+ throw new Error('learning worker has an open batch; settle or abandon that batch first');
168
+ }
169
+ const updated = await this.service.ledger.putLearningLaunch({ ...launch,
170
+ status: 'abandoned', reason: explanation, updatedAt: nowIso() });
171
+ return this.view(updated);
172
+ });
173
+ }
174
+ /**
175
+ * Abandon launches that have needed review for longer than `maxAgeMs`, so one unresolved launch
176
+ * cannot block every later scheduled learning period. A live worker or an open batch keeps it.
177
+ */
178
+ async retireStaleReviews(homeAddress, now, maxAgeMs) {
179
+ return this.service.ledger.withLearningBatchWrite(async () => {
180
+ const retired = [];
181
+ const openWorkers = new Set(this.service.ledger.listLearningBatches(homeAddress)
182
+ .filter(run => run.status === 'open').map(run => run.reviewerSessionId));
183
+ for (const launch of this.service.ledger.listLearningLaunches(homeAddress)) {
184
+ if (launch.status !== 'needs_review' || Date.parse(launch.updatedAt) > now - maxAgeMs)
185
+ continue;
186
+ if (this.agents.get(launch.workerSessionId) || openWorkers.has(launch.workerSessionId))
187
+ continue;
188
+ await this.service.ledger.putLearningLaunch({ ...launch, status: 'abandoned',
189
+ reason: `automatically abandoned after needing review since ${launch.updatedAt}`
190
+ + (launch.reason ? `: ${launch.reason}` : ''),
191
+ updatedAt: new Date(now).toISOString() });
192
+ retired.push(launch.id);
193
+ }
194
+ return retired;
195
+ });
196
+ }
197
+ /** On restart, only an already visible worker Session may be resumed. */
198
+ async recoverBatch(limit = RECOVERY_MAX) {
199
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > RECOVERY_MAX) {
200
+ throw new Error(`learning Session recovery limit must be between 1 and ${RECOVERY_MAX}`);
201
+ }
202
+ const recovered = [];
203
+ const needsReview = [];
204
+ const failed = [];
205
+ for (const launch of this.service.ledger.listLearningLaunches()
206
+ .filter(item => (item.status === 'reserved' || item.status === 'active')
207
+ && !this.watching.has(item.id)
208
+ && (this.recoveryRetryAfter.get(item.id) ?? 0) <= Date.now())
209
+ .reverse().slice(0, limit)) {
210
+ if (this.closing)
211
+ break;
212
+ if (this.activating.has(launch.id))
213
+ continue;
214
+ try {
215
+ const result = await this.activateOnce(launch, false);
216
+ this.recoveryRetryAfter.delete(launch.id);
217
+ if (result.launch.status === 'needs_review')
218
+ needsReview.push(launch.id);
219
+ else
220
+ recovered.push(launch.id);
221
+ }
222
+ catch (error) {
223
+ this.recoveryRetryAfter.set(launch.id, Date.now() + RETRY_DELAY_MS);
224
+ failed.push({ id: launch.id, error });
225
+ }
226
+ }
227
+ return { recovered, needsReview, failed };
228
+ }
229
+ async close() {
230
+ this.closing = true;
231
+ await Promise.allSettled([...this.activating.values()]);
232
+ const handles = [...this.handles.values()];
233
+ this.handles.clear();
234
+ await Promise.allSettled(handles.map(handle => handle.dispose()));
235
+ }
236
+ activateOnce(launch, allowCreate) {
237
+ const pending = this.activating.get(launch.id);
238
+ if (pending)
239
+ return pending;
240
+ const task = this.activate(launch, allowCreate);
241
+ this.activating.set(launch.id, task);
242
+ void task.finally(() => { if (this.activating.get(launch.id) === task)
243
+ this.activating.delete(launch.id); }).catch(() => { });
244
+ return task;
245
+ }
246
+ async activate(launch, allowCreate) {
247
+ if (this.closing)
248
+ throw new Error('learning Session runner is closing');
249
+ const current = this.service.ledger.getLearningLaunch(launch.id);
250
+ if (!current || !['reserved', 'active'].includes(current.status))
251
+ return this.view(current ?? launch);
252
+ if (this.service.store.getHome(current.homeAddress).status !== 'active') {
253
+ return this.markNeedsReview(current, 'learning Home is no longer active');
254
+ }
255
+ try {
256
+ await realDirectory(current.workspace);
257
+ }
258
+ catch (error) {
259
+ return this.markNeedsReview(current, `learning Workspace is unavailable: ${message(error)}`);
260
+ }
261
+ const mapping = this.config.workspaceHomes.find(item => item.address === current.homeAddress);
262
+ if (mapping?.root !== current.workspace) {
263
+ return this.markNeedsReview(current, 'learning Workspace mapping changed');
264
+ }
265
+ const sessionId = current.workerSessionId;
266
+ const oldBinding = this.service.ledger.getBinding(sessionId);
267
+ if (oldBinding && (oldBinding.homeAddress !== current.homeAddress || oldBinding.cwd !== current.workspace)) {
268
+ throw new Error('learning worker Session binding conflicts with its reservation');
269
+ }
270
+ await this.service.ledger.putBinding({ sessionId, homeAddress: current.homeAddress,
271
+ cwd: current.workspace, boundAt: oldBinding?.boundAt ?? nowIso() });
272
+ let handle = this.handles.get(current.id);
273
+ if (handle && this.agents.get(sessionId) !== handle.agent) {
274
+ this.handles.delete(current.id);
275
+ handle = undefined;
276
+ }
277
+ let worker = handle?.agent ?? this.agents.get(sessionId);
278
+ let resumedPersisted = false;
279
+ if (!worker) {
280
+ const record = (await this.sessions.listSessions()).find(item => String(item.header.id) === sessionId);
281
+ if (record && !record.persisted)
282
+ return this.markNeedsReview(current, 'learning Session exists but is not persisted');
283
+ if (!record && !allowCreate)
284
+ return this.markNeedsReview(current, 'reserved learning Session is absent after restart');
285
+ const agentOptions = current.model || current.provider || current.maxTokens
286
+ ? { ...(current.provider ? { provider: current.provider } : {}),
287
+ ...(current.model ? { model: current.model } : {}),
288
+ ...(current.maxTokens ? { maxTokens: current.maxTokens } : {}) }
289
+ : undefined;
290
+ handle = record
291
+ ? await this.agents.resume({ resumeSessionId: sessionId,
292
+ ...(agentOptions ? { agentOptions } : {}) })
293
+ : await this.agents.create({ sessionId, meta: { cwd: current.workspace },
294
+ ...(agentOptions ? { agentOptions } : {}) });
295
+ resumedPersisted = Boolean(record);
296
+ this.handles.set(current.id, handle);
297
+ worker = handle.agent;
298
+ }
299
+ if (worker.session.header.cwd !== current.workspace) {
300
+ if (handle) {
301
+ this.handles.delete(current.id);
302
+ await handle.dispose();
303
+ }
304
+ return this.markNeedsReview(current, 'learning Session Workspace differs from its reservation');
305
+ }
306
+ const batch = this.service.ledger.listLearningBatches(current.homeAddress)
307
+ .find(run => run.reviewerSessionId === current.workerSessionId);
308
+ if (batch?.status === 'completed') {
309
+ const complete = await this.service.ledger.putLearningLaunch({ ...current,
310
+ status: 'completed', updatedAt: nowIso() });
311
+ this.releaseWhenIdle(current.id);
312
+ return this.view(complete);
313
+ }
314
+ if (batch?.status === 'abandoned')
315
+ return this.markNeedsReview(current, 'learning batch was abandoned');
316
+ const promptId = current.promptMessageId;
317
+ const wasSent = hasPrompt(worker, promptId);
318
+ if (!wasSent) {
319
+ worker.followup(freezeMessage({
320
+ id: promptId, role: 'user',
321
+ content: [{ type: 'text', text: renderPrompt(current) }],
322
+ source: { kind: 'bizagent', form: 'relay' },
323
+ }));
324
+ }
325
+ if (resumedPersisted && wasSent && batch?.status === 'open') {
326
+ const resumeId = stableId('learning-resume-prompt', current.id);
327
+ if (!hasPrompt(worker, resumeId)) {
328
+ worker.followup(freezeMessage({
329
+ id: resumeId, role: 'user',
330
+ content: [{ type: 'text', text: renderResumePrompt(current, batch.id) }],
331
+ source: { kind: 'bizagent', form: 'relay' },
332
+ }));
333
+ }
334
+ }
335
+ const active = this.service.ledger.getLearningLaunch(current.id);
336
+ const updated = active?.status === 'completed' ? active : await this.service.ledger.putLearningLaunch({
337
+ ...current, status: 'active', updatedAt: nowIso(),
338
+ });
339
+ this.watch(updated, worker);
340
+ return { ...this.view(updated), promptAlreadySent: wasSent, workerStatus: worker.status };
341
+ }
342
+ watch(launch, worker) {
343
+ if (this.watching.has(launch.id))
344
+ return;
345
+ this.watching.add(launch.id);
346
+ void worker.whenIdle().then(async () => {
347
+ const current = this.service.ledger.getLearningLaunch(launch.id);
348
+ if (!current || !['reserved', 'active'].includes(current.status))
349
+ return;
350
+ const batch = this.service.ledger.listLearningBatches(current.homeAddress)
351
+ .find(run => run.reviewerSessionId === current.workerSessionId);
352
+ if (batch?.status === 'completed') {
353
+ await this.service.ledger.putLearningLaunch({ ...current, status: 'completed', updatedAt: nowIso() });
354
+ }
355
+ else {
356
+ const recent = worker.session.snapshotEvents().slice(-6).map(event => event.type).join(', ');
357
+ const pending = worker.inbox.nextTurn.length + worker.inbox.nextStep.length;
358
+ await this.markNeedsReview(current, `learning Session became idle without a completed batch (recent events: ${recent || 'none'}; pending messages: ${pending})`);
359
+ }
360
+ }).catch(async (error) => {
361
+ this.service.recordRuntimeError(`learning worker ${launch.id}`, error);
362
+ await this.markNeedsReview(launch, `learning Session idle status failed: ${message(error)}`);
363
+ })
364
+ .finally(async () => {
365
+ this.watching.delete(launch.id);
366
+ const handle = this.handles.get(launch.id);
367
+ if (handle) {
368
+ this.handles.delete(launch.id);
369
+ await handle.dispose();
370
+ }
371
+ }).catch(error => this.service.recordRuntimeError(`learning worker cleanup ${launch.id}`, error));
372
+ }
373
+ releaseWhenIdle(id) {
374
+ const handle = this.handles.get(id);
375
+ if (!handle)
376
+ return;
377
+ void handle.agent.whenIdle().then(async () => {
378
+ if (this.handles.get(id) === handle) {
379
+ this.handles.delete(id);
380
+ await handle.dispose();
381
+ }
382
+ }).catch(error => this.service.recordRuntimeError(`learning worker dispose ${id}`, error));
383
+ }
384
+ async markNeedsReview(launch, reason) {
385
+ const current = this.service.ledger.getLearningLaunch(launch.id);
386
+ if (!current || !['reserved', 'active'].includes(current.status))
387
+ return this.view(current ?? launch);
388
+ const updated = await this.service.ledger.putLearningLaunch({ ...current,
389
+ status: 'needs_review', reason, updatedAt: nowIso() });
390
+ return this.view(updated);
391
+ }
392
+ view(launch) {
393
+ const batch = this.service.ledger.listLearningBatches(launch.homeAddress)
394
+ .find(run => run.reviewerSessionId === launch.workerSessionId);
395
+ return { launch, batch };
396
+ }
397
+ }
398
+ async function realDirectory(path) {
399
+ const info = await stat(path);
400
+ if (!info.isDirectory() || await realpath(path) !== path)
401
+ throw new Error(`learning Workspace is not a real directory: ${path}`);
402
+ }
403
+ function renderPrompt(launch) {
404
+ return [
405
+ '# Dedicated learning review for this Agent Home',
406
+ `Launch ID: ${launch.id}`,
407
+ `Home: ${launch.homeAddress}`,
408
+ 'Call bizagent_learning_batch_start in this Session. If no material is pending, report that and stop.',
409
+ 'For each frozen material, inspect Session events with bizagent_episode_read or the exact authored Worklog revision with bizagent_worklog_read.',
410
+ 'Use bizagent_learning_batch_settle to record a reasoned no-change review or evidence-backed candidates.',
411
+ 'Session candidates cite original event sequence fields; Worklog candidates omit those fields and cite their frozen revision. All remain candidates until later review and publication.',
412
+ 'When all materials are settled, call bizagent_learning_batch_finish with action complete.',
413
+ 'A conversation ending without the completion receipt leaves this launch needing review.',
414
+ ].join('\n');
415
+ }
416
+ function renderResumePrompt(launch, batchId) {
417
+ return [
418
+ '# Continue the interrupted learning review in this same Session',
419
+ `Launch ID: ${launch.id}`,
420
+ `Open batch ID: ${batchId}`,
421
+ 'First call bizagent_learning_batch_status for this batch. If it is completed, stop.',
422
+ 'For each pending material, read its Session evidence with bizagent_episode_read or exact Worklog with bizagent_worklog_read,',
423
+ 'then call bizagent_learning_batch_settle with a reasoned no-change review or evidence-backed candidates.',
424
+ 'Do not start a new batch or repeat materials already settled in this batch.',
425
+ 'When all frozen materials are settled, call bizagent_learning_batch_finish with action complete.',
426
+ ].join('\n');
427
+ }
428
+ function hasPrompt(worker, id) {
429
+ return worker.session.snapshotEvents().some(event => event.type === 'agent/inbox/spliced'
430
+ && event.data.inserted.some(item => item.id === id))
431
+ || worker.inbox.nextTurn.some(item => item.id === id)
432
+ || worker.inbox.nextStep.some(item => item.id === id);
433
+ }
434
+ function message(error) { return error instanceof Error ? error.message : String(error); }
435
+ //# sourceMappingURL=learning-session-runner.js.map