@hhuangpanda/dsh-automation 0.0.0-stage → 0.3.34

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 (63) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +133 -2
  3. package/README.zh.md +134 -0
  4. package/cordis.patch.yml +3 -0
  5. package/docs/images/automation-overview-en.png +0 -0
  6. package/docs/images/automation-overview-zh.png +0 -0
  7. package/docs/reference.md +55 -0
  8. package/docs/reference.zh.md +55 -0
  9. package/lib/agent-configuration.d.ts +27 -0
  10. package/lib/agent-configuration.js +217 -0
  11. package/lib/api.d.ts +5 -0
  12. package/lib/api.js +253 -0
  13. package/lib/client/AutomationPanel.d.ts +9 -0
  14. package/lib/client/EmptyState.d.ts +10 -0
  15. package/lib/client/ModelSelection.d.ts +17 -0
  16. package/lib/client/ScheduleSection.d.ts +32 -0
  17. package/lib/client/SchedulerHealth.d.ts +10 -0
  18. package/lib/client/SkillPicker.d.ts +15 -0
  19. package/lib/client/TaskDetail.d.ts +34 -0
  20. package/lib/client/TaskEditor.d.ts +38 -0
  21. package/lib/client/TaskRow.d.ts +12 -0
  22. package/lib/client/editor-layout.d.ts +20 -0
  23. package/lib/client/i18n.d.ts +15 -0
  24. package/lib/client/icons.d.ts +30 -0
  25. package/lib/client/index.d.ts +6 -0
  26. package/lib/client/locales.d.ts +260 -0
  27. package/lib/client/rrule-editor.d.ts +15 -0
  28. package/lib/client/session-selection.d.ts +4 -0
  29. package/lib/client/shared.d.ts +77 -0
  30. package/lib/client/store.d.ts +40 -0
  31. package/lib/client.js +3014 -0
  32. package/lib/controller.d.ts +36 -0
  33. package/lib/controller.js +154 -0
  34. package/lib/domain.d.ts +44 -0
  35. package/lib/domain.js +473 -0
  36. package/lib/errors.d.ts +14 -0
  37. package/lib/errors.js +16 -0
  38. package/lib/im-delivery.d.ts +30 -0
  39. package/lib/im-delivery.js +69 -0
  40. package/lib/index.d.ts +25 -0
  41. package/lib/index.js +92 -0
  42. package/lib/recurrence.d.ts +14 -0
  43. package/lib/recurrence.js +203 -0
  44. package/lib/runner.d.ts +27 -0
  45. package/lib/runner.js +295 -0
  46. package/lib/runtime-marker.d.ts +3 -0
  47. package/lib/runtime-marker.js +2 -0
  48. package/lib/scheduler.d.ts +58 -0
  49. package/lib/scheduler.js +287 -0
  50. package/lib/store.d.ts +14 -0
  51. package/lib/store.js +108 -0
  52. package/lib/tool-configuration.d.ts +8 -0
  53. package/lib/tool-configuration.js +220 -0
  54. package/lib/tool-support.d.ts +67 -0
  55. package/lib/tool-support.js +47 -0
  56. package/lib/tools.d.ts +6 -0
  57. package/lib/tools.js +140 -0
  58. package/lib/types.d.ts +465 -0
  59. package/lib/types.js +99 -0
  60. package/lib/validation.d.ts +34 -0
  61. package/lib/validation.js +99 -0
  62. package/package.json +163 -4
  63. package/screenshots.json +1 -0
@@ -0,0 +1,58 @@
1
+ import type { AutomationTask, AutomationRun, AutomationSchedulerHealth } from './types.js';
2
+ import { AutomationDomain, type RunOutcome } from './domain.js';
3
+ export declare const MAX_TIMER_DELAY_MS = 2147483647;
4
+ export declare const RETRY_BASE_DELAY_MS = 1000;
5
+ export declare const RETRY_MAX_DELAY_MS = 60000;
6
+ export declare const DEFAULT_MAX_RUN_DURATION_MS: number;
7
+ export declare const DELIVERY_TIMEOUT_MS = 15000;
8
+ export type AutomationDeliverySender = (task: AutomationTask, run: AutomationRun, outcome: RunOutcome, signal: AbortSignal) => Promise<void>;
9
+ export type AutomationRunCancelReason = 'manual' | 'timeout' | 'shutdown';
10
+ export interface Clock {
11
+ now(): number;
12
+ setTimeout(callback: () => void, delay: number): () => void;
13
+ }
14
+ export interface AutomationRunnerResult {
15
+ readonly status: 'succeeded' | 'failed';
16
+ readonly sessionId: string;
17
+ readonly summary?: string;
18
+ readonly output?: string;
19
+ readonly error?: string;
20
+ }
21
+ export interface AutomationRunner {
22
+ run(task: AutomationTask, run: AutomationRun): Promise<AutomationRunnerResult>;
23
+ cancel?(runId: string, reason: AutomationRunCancelReason): boolean;
24
+ }
25
+ export declare const systemClock: Clock;
26
+ export declare class AutomationScheduler {
27
+ readonly domain: AutomationDomain;
28
+ readonly runner: AutomationRunner;
29
+ readonly clock: Clock;
30
+ private readonly onError;
31
+ readonly maxRunDurationMs: number;
32
+ private readonly sender?;
33
+ private cancelTimer;
34
+ private generation;
35
+ private requested;
36
+ private stopped;
37
+ private driving;
38
+ private healthState;
39
+ private pendingFinish;
40
+ private activeDelivery;
41
+ private activeRun;
42
+ constructor(domain: AutomationDomain, runner: AutomationRunner, clock?: Clock, onError?: (error: unknown) => void, maxRunDurationMs?: number, sender?: AutomationDeliverySender | undefined);
43
+ start(): void;
44
+ health(): AutomationSchedulerHealth;
45
+ cancelRun(taskId: string, runId: string): boolean;
46
+ requestDrive(): void;
47
+ whenSettled(): Promise<void>;
48
+ stop(): Promise<void>;
49
+ private markHealthy;
50
+ private scheduleRetry;
51
+ private cancelActiveRun;
52
+ private clearTimer;
53
+ private arm;
54
+ private finishPendingRun;
55
+ private sendDelivery;
56
+ private drive;
57
+ }
58
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1,287 @@
1
+ import { AutomationDomain } from './domain.js';
2
+ export const MAX_TIMER_DELAY_MS = 2_147_483_647;
3
+ export const RETRY_BASE_DELAY_MS = 1_000;
4
+ export const RETRY_MAX_DELAY_MS = 60_000;
5
+ export const DEFAULT_MAX_RUN_DURATION_MS = 60 * 60_000;
6
+ export const DELIVERY_TIMEOUT_MS = 15_000;
7
+ export const systemClock = {
8
+ now: () => Date.now(),
9
+ setTimeout(callback, delay) {
10
+ const handle = setTimeout(callback, delay);
11
+ return () => clearTimeout(handle);
12
+ },
13
+ };
14
+ export class AutomationScheduler {
15
+ domain;
16
+ runner;
17
+ clock;
18
+ onError;
19
+ maxRunDurationMs;
20
+ sender;
21
+ cancelTimer;
22
+ generation = 0;
23
+ requested = false;
24
+ stopped = false;
25
+ driving;
26
+ healthState = { status: 'healthy', consecutiveFailures: 0 };
27
+ pendingFinish;
28
+ activeDelivery;
29
+ activeRun;
30
+ constructor(domain, runner, clock = systemClock, onError = (error) => console.error(error), maxRunDurationMs = DEFAULT_MAX_RUN_DURATION_MS, sender) {
31
+ this.domain = domain;
32
+ this.runner = runner;
33
+ this.clock = clock;
34
+ this.onError = onError;
35
+ this.maxRunDurationMs = maxRunDurationMs;
36
+ this.sender = sender;
37
+ }
38
+ start() {
39
+ this.requestDrive();
40
+ }
41
+ health() {
42
+ return structuredClone(this.healthState);
43
+ }
44
+ cancelRun(taskId, runId) {
45
+ const active = this.activeRun;
46
+ if (active === undefined || active.taskId !== taskId || active.runId !== runId)
47
+ return false;
48
+ return this.cancelActiveRun('manual');
49
+ }
50
+ requestDrive() {
51
+ if (this.stopped)
52
+ return;
53
+ this.clearTimer();
54
+ this.requested = true;
55
+ if (this.driving !== undefined)
56
+ return;
57
+ const driving = this.drive().then(() => this.markHealthy(), (error) => this.scheduleRetry(error));
58
+ this.driving = driving;
59
+ void driving.finally(() => {
60
+ if (this.driving === driving)
61
+ this.driving = undefined;
62
+ if (this.requested && !this.stopped)
63
+ this.requestDrive();
64
+ });
65
+ }
66
+ async whenSettled() {
67
+ while (this.driving !== undefined) {
68
+ const current = this.driving;
69
+ await current;
70
+ if (this.driving === current)
71
+ await Promise.resolve();
72
+ }
73
+ }
74
+ async stop() {
75
+ this.stopped = true;
76
+ this.requested = false;
77
+ this.clearTimer();
78
+ this.cancelActiveRun('shutdown');
79
+ this.activeDelivery?.abort(new Error('DSH stopped before message delivery settled; delivery may have completed.'));
80
+ const { retryAt: _retryAt, ...health } = this.healthState;
81
+ this.healthState = { ...health, status: 'stopped' };
82
+ await this.driving;
83
+ }
84
+ markHealthy() {
85
+ if (this.stopped || this.healthState.status === 'healthy')
86
+ return;
87
+ const { lastError, lastFailedAt } = this.healthState;
88
+ this.healthState = {
89
+ status: 'healthy',
90
+ consecutiveFailures: 0,
91
+ ...(lastError === undefined ? {} : { lastError }),
92
+ ...(lastFailedAt === undefined ? {} : { lastFailedAt }),
93
+ };
94
+ }
95
+ scheduleRetry(error) {
96
+ if (this.stopped)
97
+ return;
98
+ this.requested = false;
99
+ const now = this.clock.now();
100
+ const consecutiveFailures = this.healthState.consecutiveFailures + 1;
101
+ const delay = Math.min(RETRY_BASE_DELAY_MS * (2 ** (consecutiveFailures - 1)), RETRY_MAX_DELAY_MS);
102
+ this.healthState = {
103
+ status: 'retrying',
104
+ consecutiveFailures,
105
+ lastError: error instanceof Error ? error.message : String(error),
106
+ lastFailedAt: new Date(now).toISOString(),
107
+ retryAt: new Date(now + delay).toISOString(),
108
+ };
109
+ try {
110
+ this.onError(error);
111
+ }
112
+ catch { }
113
+ this.arm(now + delay);
114
+ }
115
+ cancelActiveRun(reason) {
116
+ const active = this.activeRun;
117
+ if (active === undefined)
118
+ return false;
119
+ if (active.cancelReason !== undefined)
120
+ return true;
121
+ if (this.runner.cancel?.(active.runId, reason) !== true)
122
+ return false;
123
+ active.cancelReason = reason;
124
+ active.cancelTimeout?.();
125
+ active.cancelTimeout = undefined;
126
+ return true;
127
+ }
128
+ clearTimer() {
129
+ this.generation += 1;
130
+ this.cancelTimer?.();
131
+ this.cancelTimer = undefined;
132
+ }
133
+ arm(target) {
134
+ const now = this.clock.now();
135
+ const delay = Math.max(0, Math.min(target - now, MAX_TIMER_DELAY_MS));
136
+ const generation = ++this.generation;
137
+ this.cancelTimer = this.clock.setTimeout(() => {
138
+ if (this.stopped || generation !== this.generation)
139
+ return;
140
+ this.cancelTimer = undefined;
141
+ this.requestDrive();
142
+ }, delay);
143
+ }
144
+ async finishPendingRun(now) {
145
+ const pending = this.pendingFinish;
146
+ if (pending === undefined)
147
+ return;
148
+ if (!pending.executionSaved) {
149
+ // Record execution and send intent together before any external side effect.
150
+ await this.domain.finishRun(pending.task.id, pending.run.id, pending.outcome, now, pending.task.delivery);
151
+ pending.executionSaved = true;
152
+ }
153
+ if (pending.deliveryResult === undefined) {
154
+ // The task/history may have been removed while persistence was retried.
155
+ const savedRun = this.domain.store.snapshot().tasks[pending.task.id]?.runs.find((run) => run.id === pending.run.id);
156
+ if (savedRun?.delivery?.status === 'sending'
157
+ && ['succeeded', 'failed', 'timed_out'].includes(savedRun.status)
158
+ && savedRun.delivery.botId === pending.task.delivery?.botId
159
+ && savedRun.delivery.targetId === pending.task.delivery?.targetId) {
160
+ pending.deliveryResult = await this.sendDelivery(pending.task, savedRun, pending.outcome, savedRun.delivery);
161
+ }
162
+ }
163
+ if (pending.deliveryResult !== undefined) {
164
+ // Retrying this write must never call the sender (or the Agent) again.
165
+ await this.domain.finishDelivery(pending.task.id, pending.run.id, pending.deliveryResult);
166
+ }
167
+ if (this.pendingFinish === pending)
168
+ this.pendingFinish = undefined;
169
+ }
170
+ async sendDelivery(task, run, outcome, delivery) {
171
+ const controller = new AbortController();
172
+ this.activeDelivery = controller;
173
+ let cancelTimeout;
174
+ let onAbort;
175
+ let status = 'sent';
176
+ let error;
177
+ try {
178
+ const aborted = new Promise((_resolve, reject) => {
179
+ onAbort = () => reject(controller.signal.reason);
180
+ controller.signal.addEventListener('abort', onAbort, { once: true });
181
+ });
182
+ cancelTimeout = this.clock.setTimeout(() => {
183
+ controller.abort(new Error('Message delivery timed out; it may have completed and will not be resent automatically.'));
184
+ }, DELIVERY_TIMEOUT_MS);
185
+ if (this.stopped)
186
+ controller.abort(new Error('DSH stopped before message delivery started.'));
187
+ await Promise.race([
188
+ aborted,
189
+ Promise.resolve().then(() => {
190
+ controller.signal.throwIfAborted();
191
+ const saved = this.domain.store.snapshot().tasks[task.id]?.runs.find((entry) => entry.id === run.id)?.delivery;
192
+ if (saved?.status !== 'sending' || saved.botId !== delivery.botId || saved.targetId !== delivery.targetId || saved.attemptedAt !== delivery.attemptedAt)
193
+ return;
194
+ if (this.sender === undefined)
195
+ throw new Error('Message delivery service is unavailable.');
196
+ return this.sender(task, run, outcome, controller.signal);
197
+ }),
198
+ ]);
199
+ }
200
+ catch (cause) {
201
+ status = controller.signal.aborted ? 'unknown' : 'failed';
202
+ error = cause instanceof Error ? cause.message : String(cause);
203
+ if (!error)
204
+ error = 'Message delivery failed.';
205
+ }
206
+ finally {
207
+ cancelTimeout?.();
208
+ if (onAbort !== undefined)
209
+ controller.signal.removeEventListener('abort', onAbort);
210
+ if (this.activeDelivery === controller)
211
+ this.activeDelivery = undefined;
212
+ }
213
+ return { ...delivery, status, finishedAt: new Date(this.clock.now()).toISOString(), ...(error === undefined ? {} : { error }) };
214
+ }
215
+ async drive() {
216
+ while (this.requested && !this.stopped) {
217
+ this.requested = false;
218
+ if (this.pendingFinish !== undefined) {
219
+ await this.finishPendingRun(this.clock.now());
220
+ this.requested = true;
221
+ continue;
222
+ }
223
+ const now = this.clock.now();
224
+ await this.domain.claimDue(now);
225
+ const claimed = await this.domain.takeNextQueued(this.clock.now());
226
+ if (claimed !== undefined) {
227
+ const active = {
228
+ taskId: claimed.task.id,
229
+ runId: claimed.run.id,
230
+ cancelReason: undefined,
231
+ cancelTimeout: undefined,
232
+ };
233
+ this.activeRun = active;
234
+ active.cancelTimeout = this.clock.setTimeout(() => {
235
+ active.cancelTimeout = undefined;
236
+ if (this.activeRun === active)
237
+ this.cancelActiveRun('timeout');
238
+ }, Math.min(this.maxRunDurationMs, MAX_TIMER_DELAY_MS));
239
+ let outcome;
240
+ try {
241
+ const result = await this.runner.run(claimed.task, claimed.run);
242
+ outcome = {
243
+ status: result.status,
244
+ sessionId: result.sessionId,
245
+ ...(result.summary === undefined ? {} : { summary: result.summary }),
246
+ ...(result.output === undefined ? {} : { output: result.output }),
247
+ ...(result.error === undefined ? {} : { error: result.error }),
248
+ };
249
+ }
250
+ catch (error) {
251
+ outcome = {
252
+ status: 'failed',
253
+ error: error instanceof Error ? error.message : String(error),
254
+ };
255
+ }
256
+ finally {
257
+ active.cancelTimeout?.();
258
+ active.cancelTimeout = undefined;
259
+ }
260
+ const resultData = {
261
+ ...(outcome.sessionId === undefined ? {} : { sessionId: outcome.sessionId }),
262
+ ...(outcome.summary === undefined ? {} : { summary: outcome.summary }),
263
+ ...(outcome.output === undefined ? {} : { output: outcome.output }),
264
+ };
265
+ if (active.cancelReason === 'timeout') {
266
+ outcome = { status: 'timed_out', error: `Automation exceeded its ${this.maxRunDurationMs}ms run limit.`, ...resultData };
267
+ }
268
+ else if (active.cancelReason === 'manual') {
269
+ outcome = { status: 'canceled', error: 'Automation run was stopped by the user.', ...resultData };
270
+ }
271
+ else if (active.cancelReason === 'shutdown') {
272
+ outcome = { status: 'interrupted', error: 'DSH stopped while this automation run was active.', ...resultData };
273
+ }
274
+ if (this.activeRun === active)
275
+ this.activeRun = undefined;
276
+ this.pendingFinish = { task: claimed.task, run: claimed.run, outcome, executionSaved: false };
277
+ await this.finishPendingRun(this.clock.now());
278
+ this.requested = true;
279
+ continue;
280
+ }
281
+ const target = this.domain.nextWakeAt(this.clock.now());
282
+ if (target !== undefined)
283
+ this.arm(target);
284
+ }
285
+ }
286
+ }
287
+ //# sourceMappingURL=scheduler.js.map
package/lib/store.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type AutomationState } from './types.js';
2
+ export type AtomicWriter = (path: string, content: string) => Promise<void>;
3
+ export declare function writeJsonAtomic(path: string, content: string): Promise<void>;
4
+ export declare class AutomationStore {
5
+ readonly path: string;
6
+ private readonly writer;
7
+ private state;
8
+ private tail;
9
+ constructor(path: string, writer?: AtomicWriter);
10
+ init(): Promise<void>;
11
+ snapshot(): AutomationState;
12
+ mutate<T>(operation: (draft: AutomationState) => T | Promise<T>): Promise<T>;
13
+ }
14
+ //# sourceMappingURL=store.d.ts.map
package/lib/store.js ADDED
@@ -0,0 +1,108 @@
1
+ import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { AutomationStateSchema, emptyAutomationState } from './types.js';
5
+ export async function writeJsonAtomic(path, content) {
6
+ const directory = dirname(path);
7
+ await mkdir(directory, { recursive: true });
8
+ const temporary = join(directory, `.${basename(path)}.${randomUUID()}.tmp`);
9
+ let handle;
10
+ try {
11
+ handle = await open(temporary, 'wx', 0o600);
12
+ await handle.writeFile(content, 'utf8');
13
+ await handle.sync();
14
+ await handle.close();
15
+ handle = undefined;
16
+ await rename(temporary, path);
17
+ // Windows cannot fsync a read-only directory handle. File contents above
18
+ // are still synced before the atomic rename on every platform.
19
+ if (process.platform !== 'win32') {
20
+ try {
21
+ const directoryHandle = await open(directory, 'r');
22
+ try {
23
+ await directoryHandle.sync();
24
+ }
25
+ finally {
26
+ await directoryHandle.close();
27
+ }
28
+ }
29
+ catch (error) {
30
+ // Rename committed the new state. Rejecting now would leave the store's
31
+ // memory stale and allow a later mutation to overwrite that commit.
32
+ process.emitWarning(`Automation state was committed to ${path}, but directory sync failed; crash durability is not guaranteed.`, {
33
+ code: 'AUTOMATION_DIRECTORY_SYNC_FAILED',
34
+ detail: error instanceof Error ? error.stack ?? error.message : String(error),
35
+ });
36
+ }
37
+ }
38
+ }
39
+ finally {
40
+ if (handle !== undefined)
41
+ await handle.close().catch(() => undefined);
42
+ await rm(temporary, { force: true }).catch(() => undefined);
43
+ }
44
+ }
45
+ export class AutomationStore {
46
+ path;
47
+ writer;
48
+ state;
49
+ tail = Promise.resolve();
50
+ constructor(path, writer = writeJsonAtomic) {
51
+ this.path = path;
52
+ this.writer = writer;
53
+ }
54
+ async init() {
55
+ if (this.state !== undefined)
56
+ return;
57
+ let raw;
58
+ try {
59
+ raw = await readFile(this.path, 'utf8');
60
+ }
61
+ catch (error) {
62
+ if (error.code !== 'ENOENT')
63
+ throw error;
64
+ this.state = emptyAutomationState();
65
+ return;
66
+ }
67
+ let value;
68
+ try {
69
+ value = JSON.parse(raw);
70
+ }
71
+ catch (error) {
72
+ throw new Error(`Automation state is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
73
+ }
74
+ const parsed = AutomationStateSchema.safeParse(value);
75
+ if (!parsed.success)
76
+ throw new Error(`Automation state is invalid: ${parsed.error.message}`);
77
+ this.state = normalizeState(parsed.data);
78
+ }
79
+ snapshot() {
80
+ if (this.state === undefined)
81
+ throw new Error('AutomationStore.init() must complete before use.');
82
+ return structuredClone(this.state);
83
+ }
84
+ mutate(operation) {
85
+ const run = this.tail.then(async () => {
86
+ if (this.state === undefined)
87
+ throw new Error('AutomationStore.init() must complete before use.');
88
+ const draft = structuredClone(this.state);
89
+ const result = await operation(draft);
90
+ draft.revision = this.state.revision + 1;
91
+ const validated = AutomationStateSchema.parse(draft);
92
+ await this.writer(this.path, `${JSON.stringify(validated, null, 2)}\n`);
93
+ this.state = validated;
94
+ return result;
95
+ });
96
+ this.tail = run.then(() => undefined, () => undefined);
97
+ return run;
98
+ }
99
+ }
100
+ function normalizeState(state) {
101
+ for (const task of Object.values(state.tasks)) {
102
+ task.execution.target ??= { mode: 'fresh' };
103
+ for (const run of task.runs)
104
+ run.executionTarget ??= { mode: 'fresh' };
105
+ }
106
+ return state;
107
+ }
108
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,8 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import type { AutomationController } from './controller.js';
4
+ import type { AgentConfiguration } from './agent-configuration.js';
5
+ import '@deepseek-ai/dsh-agent-preset-registry';
6
+ import '@deepseek-ai/dsh-workspace';
7
+ export declare function registerConfigurationTools(rootCtx: Context, toolCtx: Context, agent: Agent, controller: AutomationController, agentConfiguration: AgentConfiguration): Array<() => void>;
8
+ //# sourceMappingURL=tool-configuration.d.ts.map