@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
package/lib/index.js ADDED
@@ -0,0 +1,92 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+ import { AutomationStore } from './store.js';
4
+ import { AutomationDomain } from './domain.js';
5
+ import { AutomationScheduler, DEFAULT_MAX_RUN_DURATION_MS, MAX_TIMER_DELAY_MS } from './scheduler.js';
6
+ import { DshAutomationRunner } from './runner.js';
7
+ import { AutomationController, validatePersistedSessionTarget } from './controller.js';
8
+ import { registerAutomationTools } from './tools.js';
9
+ import { registerAutomationApi } from './api.js';
10
+ import { AgentConfiguration } from './agent-configuration.js';
11
+ import { deliveryOptions, sendAutomationResult, validateDelivery } from './im-delivery.js';
12
+ import '@deepseek-ai/dsh-agent-preset-registry';
13
+ import '@deepseek-ai/dsh-host-webserver';
14
+ import '@deepseek-ai/dsh-llm';
15
+ import '@deepseek-ai/dsh-permission-presets';
16
+ import '@deepseek-ai/dsh-session-title';
17
+ import '@deepseek-ai/dsh-workspace';
18
+ import '@deepseek-ai/dsh-skill';
19
+ export * from './types.js';
20
+ export * from './errors.js';
21
+ export * from './recurrence.js';
22
+ export * from './store.js';
23
+ export * from './domain.js';
24
+ export * from './scheduler.js';
25
+ export * from './controller.js';
26
+ export const name = 'automation';
27
+ export const inject = [
28
+ 'agents',
29
+ 'sessions',
30
+ 'tools',
31
+ 'agentPresets',
32
+ 'llm',
33
+ 'skills',
34
+ 'permissionPresets',
35
+ 'workspaceRegistry',
36
+ 'sessionTitle',
37
+ 'webServer',
38
+ 'connection',
39
+ ];
40
+ export async function apply(ctx, config = {}) {
41
+ const root = resolve(config.root ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'automation'));
42
+ const maxRunDurationMs = config.maxRunDurationMs ?? DEFAULT_MAX_RUN_DURATION_MS;
43
+ if (!Number.isSafeInteger(maxRunDurationMs) || maxRunDurationMs <= 0 || maxRunDurationMs > MAX_TIMER_DELAY_MS) {
44
+ throw new Error(`maxRunDurationMs must be an integer between 1 and ${MAX_TIMER_DELAY_MS}.`);
45
+ }
46
+ if (ctx.permissionPresets.names.length === 0)
47
+ throw new Error('The Host exposes no permission presets.');
48
+ ctx.permissionPresets.resolve(ctx.permissionPresets.defaultPreset);
49
+ const store = new AutomationStore(join(root, 'state.json'));
50
+ const domain = new AutomationDomain(store, config.maxRunHistory);
51
+ await domain.init(Date.now());
52
+ const agentConfiguration = new AgentConfiguration(ctx);
53
+ const runner = new DshAutomationRunner(ctx, agentConfiguration);
54
+ const scheduler = new AutomationScheduler(domain, runner, undefined, (error) => {
55
+ ctx.logger.error(`automation scheduler failed and will retry: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
56
+ }, maxRunDurationMs, (task, run, outcome, signal) => sendAutomationResult(ctx, task, run, outcome, signal));
57
+ const controller = new AutomationController(domain, scheduler, undefined, agentConfiguration, (task, sessionId) => validatePersistedSessionTarget(ctx, task, sessionId), {
58
+ options: (botId) => deliveryOptions(ctx, botId),
59
+ validate: (destination) => validateDelivery(ctx, destination),
60
+ });
61
+ const toolCleanups = new Map();
62
+ const installTools = (agent) => {
63
+ // Normal resumed sessions remain usable by their human owner. Tool execution
64
+ // is guarded per automation turn rather than permanently omitting the tools.
65
+ if (agent.id.startsWith('automation-') || toolCleanups.has(agent))
66
+ return;
67
+ const dispose = registerAutomationTools(ctx, agent.ctx, agent, controller);
68
+ toolCleanups.set(agent, dispose);
69
+ };
70
+ for (const agent of ctx.agents.roots())
71
+ installTools(agent);
72
+ const stopCreated = ctx.on('agent/created', ({ agent }) => {
73
+ installTools(agent);
74
+ return undefined;
75
+ });
76
+ const stopDisposed = ctx.on('agent/disposed', ({ agent }) => {
77
+ toolCleanups.get(agent)?.();
78
+ toolCleanups.delete(agent);
79
+ });
80
+ const stopApi = registerAutomationApi(ctx, controller);
81
+ scheduler.start();
82
+ ctx.effect(() => async () => {
83
+ stopApi();
84
+ stopCreated();
85
+ stopDisposed();
86
+ for (const dispose of toolCleanups.values())
87
+ dispose();
88
+ toolCleanups.clear();
89
+ await scheduler.stop();
90
+ }, 'automation.lifecycle()');
91
+ }
92
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ import type { RRule } from 'rrule';
2
+ import type { AutomationSchedule, RecurringSchedule } from './types.js';
3
+ export declare class ScheduleInputError extends Error {
4
+ readonly code: 'invalid_schedule' | 'invalid_time_zone' | 'not_future';
5
+ constructor(code: 'invalid_schedule' | 'invalid_time_zone' | 'not_future', message: string);
6
+ }
7
+ export declare function instant(epoch: number): string;
8
+ export declare function validateTimeZone(timeZone: string): string;
9
+ export declare function buildRule(schedule: RecurringSchedule): RRule;
10
+ export declare function validateSchedule(schedule: AutomationSchedule, now: number): AutomationSchedule;
11
+ export declare function nextOccurrence(schedule: AutomationSchedule, after: number): number | undefined;
12
+ export declare function firstOccurrence(schedule: AutomationSchedule): number | undefined;
13
+ export declare function latestDueOccurrence(schedule: AutomationSchedule, floor: number, now: number): number | undefined;
14
+ //# sourceMappingURL=recurrence.d.ts.map
@@ -0,0 +1,203 @@
1
+ import RRulePackage from 'rrule';
2
+ import { ISO_INSTANT, LOCAL_DATE_TIME } from './types.js';
3
+ const { rrulestr } = RRulePackage;
4
+ export class ScheduleInputError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = 'ScheduleInputError';
10
+ }
11
+ }
12
+ export function instant(epoch) {
13
+ if (!Number.isFinite(epoch))
14
+ throw new ScheduleInputError('invalid_schedule', 'Time is not representable.');
15
+ const value = new Date(epoch).toISOString();
16
+ if (!ISO_INSTANT.test(value))
17
+ throw new ScheduleInputError('invalid_schedule', 'Time must use a four-digit UTC year.');
18
+ return value;
19
+ }
20
+ export function validateTimeZone(timeZone) {
21
+ if (!timeZone || timeZone.trim() !== timeZone) {
22
+ throw new ScheduleInputError('invalid_time_zone', 'timeZone must be a non-empty IANA zone without surrounding whitespace.');
23
+ }
24
+ try {
25
+ return new Intl.DateTimeFormat('en-US', { timeZone }).resolvedOptions().timeZone;
26
+ }
27
+ catch {
28
+ throw new ScheduleInputError('invalid_time_zone', `Unknown IANA time zone: ${timeZone}`);
29
+ }
30
+ }
31
+ function parseLocalDateTime(value) {
32
+ if (!LOCAL_DATE_TIME.test(value)) {
33
+ throw new ScheduleInputError('invalid_schedule', 'startAt must use YYYY-MM-DDTHH:mm:ss local wall-clock form.');
34
+ }
35
+ const [date = '', time = ''] = value.split('T');
36
+ const [year, month, day] = date.split('-').map(Number);
37
+ const [hour, minute, second] = time.split(':').map(Number);
38
+ if (year === undefined || month === undefined || day === undefined || hour === undefined || minute === undefined || second === undefined) {
39
+ throw new ScheduleInputError('invalid_schedule', 'startAt must use YYYY-MM-DDTHH:mm:ss local wall-clock form.');
40
+ }
41
+ const candidate = new Date(0);
42
+ candidate.setUTCFullYear(year, month - 1, day);
43
+ candidate.setUTCHours(hour, minute, second, 0);
44
+ if (candidate.getUTCFullYear() !== year ||
45
+ candidate.getUTCMonth() + 1 !== month ||
46
+ candidate.getUTCDate() !== day ||
47
+ candidate.getUTCHours() !== hour ||
48
+ candidate.getUTCMinutes() !== minute ||
49
+ candidate.getUTCSeconds() !== second) {
50
+ throw new ScheduleInputError('invalid_schedule', 'startAt must be a real local calendar time.');
51
+ }
52
+ return { year, month, day, hour, minute, second };
53
+ }
54
+ function normalizeRuleText(value) {
55
+ const trimmed = value.trim();
56
+ if (!trimmed || /(?:^|\n)DTSTART[:;]/i.test(trimmed)) {
57
+ throw new ScheduleInputError('invalid_schedule', 'rrule must contain a rule only; startAt and timeZone are separate fields.');
58
+ }
59
+ if (trimmed.includes('\n') || trimmed.includes('\r')) {
60
+ throw new ScheduleInputError('invalid_schedule', 'rrule must be a single RRULE line.');
61
+ }
62
+ return trimmed.replace(/^RRULE:/i, '');
63
+ }
64
+ function floatingDate(parts) {
65
+ return new Date(Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second));
66
+ }
67
+ function localPartsAt(epoch, timeZone) {
68
+ const formatter = new Intl.DateTimeFormat('en-US-u-ca-iso8601-nu-latn', {
69
+ timeZone,
70
+ year: 'numeric',
71
+ month: '2-digit',
72
+ day: '2-digit',
73
+ hour: '2-digit',
74
+ minute: '2-digit',
75
+ second: '2-digit',
76
+ hourCycle: 'h23',
77
+ });
78
+ const values = Object.fromEntries(formatter.formatToParts(new Date(epoch)).map((part) => [part.type, part.value]));
79
+ return {
80
+ year: Number(values.year),
81
+ month: Number(values.month),
82
+ day: Number(values.day),
83
+ hour: Number(values.hour),
84
+ minute: Number(values.minute),
85
+ second: Number(values.second),
86
+ };
87
+ }
88
+ function offsetAt(epoch, timeZone) {
89
+ const parts = localPartsAt(epoch, timeZone);
90
+ return floatingDate(parts).getTime() - epoch;
91
+ }
92
+ function equalParts(left, right) {
93
+ return left.year === right.year && left.month === right.month && left.day === right.day &&
94
+ left.hour === right.hour && left.minute === right.minute && left.second === right.second;
95
+ }
96
+ /** Resolve one wall-clock occurrence. Gaps are skipped; overlaps choose the earlier instant. */
97
+ function resolveLocalOccurrence(date, timeZone) {
98
+ const parts = {
99
+ year: date.getUTCFullYear(),
100
+ month: date.getUTCMonth() + 1,
101
+ day: date.getUTCDate(),
102
+ hour: date.getUTCHours(),
103
+ minute: date.getUTCMinutes(),
104
+ second: date.getUTCSeconds(),
105
+ };
106
+ const localEpoch = date.getTime();
107
+ const offsets = new Set([
108
+ offsetAt(localEpoch - 172_800_000, timeZone),
109
+ offsetAt(localEpoch, timeZone),
110
+ offsetAt(localEpoch + 172_800_000, timeZone),
111
+ ]);
112
+ const candidates = [...offsets]
113
+ .map((offset) => localEpoch - offset)
114
+ .filter((candidate) => equalParts(localPartsAt(candidate, timeZone), parts))
115
+ .sort((a, b) => a - b);
116
+ return candidates[0];
117
+ }
118
+ function floatingNow(epoch, timeZone) {
119
+ return floatingDate(localPartsAt(epoch, timeZone));
120
+ }
121
+ export function buildRule(schedule) {
122
+ const start = parseLocalDateTime(schedule.startAt);
123
+ validateTimeZone(schedule.timeZone);
124
+ const text = `DTSTART:${floatingDate(start).toISOString().replaceAll('-', '').replaceAll(':', '').replace('.000', '')}\nRRULE:${normalizeRuleText(schedule.rrule)}`;
125
+ try {
126
+ const parsed = rrulestr(text, { forceset: false });
127
+ if (!('after' in parsed) || !('before' in parsed))
128
+ throw new Error('RRULE sets are not supported');
129
+ return parsed;
130
+ }
131
+ catch (error) {
132
+ if (error instanceof ScheduleInputError)
133
+ throw error;
134
+ throw new ScheduleInputError('invalid_schedule', `Invalid RRULE: ${error instanceof Error ? error.message : String(error)}`);
135
+ }
136
+ }
137
+ export function validateSchedule(schedule, now) {
138
+ if (schedule.kind === 'once') {
139
+ const epoch = Date.parse(schedule.fireAt);
140
+ if (!ISO_INSTANT.test(schedule.fireAt) || !Number.isFinite(epoch) || new Date(epoch).toISOString() !== schedule.fireAt) {
141
+ throw new ScheduleInputError('invalid_schedule', 'fireAt must be a canonical RFC 3339 UTC instant.');
142
+ }
143
+ if (epoch <= now)
144
+ throw new ScheduleInputError('not_future', 'fireAt must be strictly in the future.');
145
+ return schedule;
146
+ }
147
+ buildRule(schedule);
148
+ return {
149
+ ...schedule,
150
+ rrule: normalizeRuleText(schedule.rrule),
151
+ timeZone: validateTimeZone(schedule.timeZone),
152
+ };
153
+ }
154
+ function nextRecurring(schedule, after) {
155
+ const rule = buildRule(schedule);
156
+ let floating = rule.after(floatingNow(after, schedule.timeZone), false);
157
+ for (let attempts = 0; floating !== null && attempts < 10_000; attempts += 1) {
158
+ const resolved = resolveLocalOccurrence(floating, schedule.timeZone);
159
+ if (resolved !== undefined && resolved > after)
160
+ return resolved;
161
+ floating = rule.after(floating, false);
162
+ }
163
+ return undefined;
164
+ }
165
+ export function nextOccurrence(schedule, after) {
166
+ if (schedule.kind === 'once') {
167
+ const target = Date.parse(schedule.fireAt);
168
+ return target > after ? target : undefined;
169
+ }
170
+ return nextRecurring(schedule, after);
171
+ }
172
+ export function firstOccurrence(schedule) {
173
+ if (schedule.kind === 'once')
174
+ return Date.parse(schedule.fireAt);
175
+ const start = floatingDate(parseLocalDateTime(schedule.startAt)).getTime();
176
+ const rule = buildRule(schedule);
177
+ let floating = rule.after(new Date(start - 1), false);
178
+ for (let attempts = 0; floating !== null && attempts < 10_000; attempts += 1) {
179
+ const resolved = resolveLocalOccurrence(floating, schedule.timeZone);
180
+ if (resolved !== undefined)
181
+ return resolved;
182
+ floating = rule.after(floating, false);
183
+ }
184
+ return undefined;
185
+ }
186
+ export function latestDueOccurrence(schedule, floor, now) {
187
+ if (floor > now)
188
+ return undefined;
189
+ if (schedule.kind === 'once') {
190
+ const target = Date.parse(schedule.fireAt);
191
+ return target >= floor && target <= now ? target : undefined;
192
+ }
193
+ const rule = buildRule(schedule);
194
+ let floating = rule.before(floatingNow(now, schedule.timeZone), true);
195
+ for (let attempts = 0; floating !== null && attempts < 10_000; attempts += 1) {
196
+ const resolved = resolveLocalOccurrence(floating, schedule.timeZone);
197
+ if (resolved !== undefined && resolved <= now)
198
+ return resolved >= floor ? resolved : undefined;
199
+ floating = rule.before(floating, false);
200
+ }
201
+ return undefined;
202
+ }
203
+ //# sourceMappingURL=recurrence.js.map
@@ -0,0 +1,27 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { AutomationRun, AutomationTask } from './types.js';
3
+ import type { AutomationRunner, AutomationRunnerResult, AutomationRunCancelReason } from './scheduler.js';
4
+ import { AgentConfiguration } from './agent-configuration.js';
5
+ import '@deepseek-ai/dsh-agent';
6
+ import '@deepseek-ai/dsh-agent-preset-registry';
7
+ import '@deepseek-ai/dsh-permission-presets';
8
+ import '@deepseek-ai/dsh-session-title';
9
+ import '@deepseek-ai/dsh-workspace';
10
+ declare module '@deepseek-ai/dsh-llm' {
11
+ interface MessageSourceMap {
12
+ 'plugin:automation': {
13
+ kind: 'plugin:automation';
14
+ };
15
+ }
16
+ }
17
+ export declare class DshAutomationRunner implements AutomationRunner {
18
+ private readonly ctx;
19
+ private readonly agentConfiguration;
20
+ private readonly active;
21
+ constructor(ctx: Context, agentConfiguration?: AgentConfiguration);
22
+ cancel(runId: string, reason: AutomationRunCancelReason): boolean;
23
+ /** Follow one identified message, not the lifetime of the whole shared Agent. */
24
+ private runTurn;
25
+ run(task: AutomationTask, run: AutomationRun): Promise<AutomationRunnerResult>;
26
+ }
27
+ //# sourceMappingURL=runner.d.ts.map
package/lib/runner.js ADDED
@@ -0,0 +1,295 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
3
+ import { SessionId } from '@deepseek-ai/dsh-session';
4
+ import { WorkspaceId } from '@deepseek-ai/dsh-workspace';
5
+ import { AutomationError } from './errors.js';
6
+ import { AgentConfiguration } from './agent-configuration.js';
7
+ import { unattendedAgents } from './runtime-marker.js';
8
+ import '@deepseek-ai/dsh-agent';
9
+ import '@deepseek-ai/dsh-agent-preset-registry';
10
+ import '@deepseek-ai/dsh-permission-presets';
11
+ import '@deepseek-ai/dsh-session-title';
12
+ import '@deepseek-ai/dsh-workspace';
13
+ const RUN_SUMMARY_MAX_CHARS = 500;
14
+ function assistantText(event) {
15
+ return event.data.message.content.filter((block) => block.type === 'text').map((block) => block.text).join('\n').trim();
16
+ }
17
+ function assistantSummary(text) {
18
+ const summary = text.replace(/\s+/g, ' ').trim();
19
+ if (!summary)
20
+ return undefined;
21
+ const characters = [...summary];
22
+ return characters.length <= RUN_SUMMARY_MAX_CHARS
23
+ ? summary
24
+ : `${characters.slice(0, RUN_SUMMARY_MAX_CHARS - 1).join('')}…`;
25
+ }
26
+ function titleFor(task, run) {
27
+ const time = run.scheduledAt ?? run.enqueuedAt;
28
+ return `[Automation] ${task.name}${run.trigger === 'manual' ? ' · Manual' : ''} · ${time.slice(0, 16).replace('T', ' ')}`;
29
+ }
30
+ function promptFor(task, run) {
31
+ return [
32
+ `This is an unattended DSH Automation run.`,
33
+ `Automation: ${task.name} (${task.id})`,
34
+ `Run: ${run.id}`,
35
+ `Trigger: ${run.trigger}`,
36
+ ...(run.scheduledAt === undefined ? [] : [`Scheduled occurrence: ${run.scheduledAt}`]),
37
+ `Permission preset: ${task.security.permissionPreset}. Do not create or modify automations from this run.`,
38
+ '',
39
+ task.prompt,
40
+ ].join('\n');
41
+ }
42
+ export class DshAutomationRunner {
43
+ ctx;
44
+ agentConfiguration;
45
+ active = new Map();
46
+ constructor(ctx, agentConfiguration = new AgentConfiguration(ctx)) {
47
+ this.ctx = ctx;
48
+ this.agentConfiguration = agentConfiguration;
49
+ }
50
+ cancel(runId, reason) {
51
+ const active = this.active.get(runId);
52
+ if (active === undefined)
53
+ return false;
54
+ if (active.cancelReason !== undefined)
55
+ return true;
56
+ const cancelPreparation = active.cancelValidation;
57
+ if (cancelPreparation === undefined && active.cancelTurn === undefined)
58
+ return false;
59
+ active.cancelReason = reason;
60
+ cancelPreparation?.();
61
+ try {
62
+ active.cancelTurn?.();
63
+ }
64
+ catch {
65
+ if (cancelPreparation !== undefined)
66
+ return true;
67
+ delete active.cancelReason;
68
+ return false;
69
+ }
70
+ return true;
71
+ }
72
+ /** Follow one identified message, not the lifetime of the whole shared Agent. */
73
+ async runTurn(agent, task, run, active, skills) {
74
+ const message = createUserMessage({
75
+ content: [{ type: 'text', text: promptFor(task, run) }],
76
+ source: { kind: 'plugin:automation' },
77
+ });
78
+ let turn;
79
+ let summary;
80
+ let output;
81
+ let settled = false;
82
+ let marked = false;
83
+ const disposers = [];
84
+ let resolveCompletion;
85
+ const completion = new Promise((resolve) => { resolveCompletion = resolve; });
86
+ const cleanup = () => {
87
+ delete active.cancelTurn;
88
+ if (marked) {
89
+ unattendedAgents.delete(agent);
90
+ marked = false;
91
+ }
92
+ for (const dispose of disposers.splice(0))
93
+ dispose();
94
+ };
95
+ const finish = (status, error) => {
96
+ if (settled)
97
+ return;
98
+ settled = true;
99
+ // Synchronous turn/end cleanup: the driver may start a human follow-up
100
+ // before the Promise continuation below gets a chance to run.
101
+ cleanup();
102
+ resolveCompletion({ status, sessionId: agent.session.header.id,
103
+ ...(summary === undefined ? {} : { summary }), ...(output === undefined ? {} : { output }),
104
+ ...(error === undefined ? {} : { error }) });
105
+ };
106
+ try {
107
+ disposers.push(this.ctx.on('agent/inbox/claimed', (event) => {
108
+ if (settled || event.agent !== agent || event.message.id !== message.id)
109
+ return;
110
+ turn = event.turn;
111
+ unattendedAgents.add(agent);
112
+ marked = true;
113
+ if (active.cancelReason !== undefined)
114
+ agent.cancel({ kind: 'hook', reason: `automation_${active.cancelReason}` }, { keepInbox: true });
115
+ }));
116
+ disposers.push(this.ctx.on('agent/inbox/discarded', (event) => {
117
+ if (event.agent === agent && event.message.id === message.id && turn === undefined) {
118
+ finish('failed', 'Automation input was removed before execution.');
119
+ }
120
+ }));
121
+ disposers.push(this.ctx.on('agent/disposed', (event) => {
122
+ if (event.agent === agent)
123
+ finish('failed', 'Automation Agent was disposed before its turn completed.');
124
+ }));
125
+ disposers.push(this.ctx.on('session/event', (session, event) => {
126
+ if (session !== agent.session || turn === undefined)
127
+ return;
128
+ if (event.type === 'assistant/message' && event.data.turn === turn) {
129
+ output = assistantText(event) || undefined;
130
+ summary = assistantSummary(output ?? '');
131
+ }
132
+ if (event.type === 'turn/end' && event.data.turn === turn) {
133
+ const reason = event.data.reason;
134
+ finish(reason.kind === 'completed' ? 'succeeded' : 'failed', reason.kind === 'completed' ? undefined
135
+ : `Automation turn ended with ${reason.kind === 'error' ? reason.error.message : reason.kind}.`);
136
+ }
137
+ }));
138
+ disposers.push(this.ctx.effect(() => () => {
139
+ if (settled)
140
+ return;
141
+ try {
142
+ active.cancelTurn?.();
143
+ }
144
+ finally {
145
+ finish('failed', 'Automation runner stopped before its turn completed.');
146
+ }
147
+ }, 'automation.turn()'));
148
+ let admitted = false;
149
+ try {
150
+ // Public status also reports maintenance as idle. The official maintenance
151
+ // reservation is the atomic true-idle check; enqueue synchronously under it.
152
+ await agent.runMaintenance(async () => {
153
+ admitted = true;
154
+ if (agent.inbox.nextTurn.length > 0 || agent.inbox.nextStep.length > 0 || unattendedAgents.has(agent)) {
155
+ throw new AutomationError('target_session_busy', 'target session has pending work.');
156
+ }
157
+ if (active.cancelReason !== undefined) {
158
+ finish('failed', `Automation run canceled before execution: ${active.cancelReason}.`);
159
+ return;
160
+ }
161
+ this.ctx.permissionPresets.set(agent.session, task.security.permissionPreset);
162
+ delete active.cancelValidation;
163
+ active.cancelTurn = () => {
164
+ if (turn === undefined) {
165
+ // Never cancel maintenance or discard someone else's queued prompt.
166
+ // A claimed notification may already be dispatching. If removal
167
+ // loses that race, the claimed listener applies the pending cancel.
168
+ if (agent.inbox.remove(message.id))
169
+ finish('failed', 'Automation input was canceled before execution.');
170
+ }
171
+ else {
172
+ agent.cancel({ kind: 'hook', reason: `automation_${active.cancelReason}` }, { keepInbox: true });
173
+ }
174
+ };
175
+ for (const skill of skills)
176
+ agent.inject(createUserMessage({ content: [{ type: 'text', text: skill.text }], source: skill.source }));
177
+ agent.followup(message);
178
+ });
179
+ }
180
+ catch (error) {
181
+ if (!admitted)
182
+ throw new AutomationError('target_session_busy', 'target session has active work or maintenance.');
183
+ throw error;
184
+ }
185
+ return await completion;
186
+ }
187
+ finally {
188
+ cleanup();
189
+ }
190
+ }
191
+ async run(task, run) {
192
+ const active = {};
193
+ const configuredTarget = task.execution.target ?? { mode: 'fresh' };
194
+ const pinned = (run.executionTarget?.mode ?? configuredTarget.mode) === 'pinned-session';
195
+ const target = pinned && configuredTarget.mode === 'pinned-session'
196
+ ? { ...configuredTarget, sessionId: run.executionTarget?.sessionId ?? configuredTarget.sessionId }
197
+ : undefined;
198
+ if (pinned && target === undefined)
199
+ throw new AutomationError('target_resume_failed', 'pinned target snapshot is unavailable.');
200
+ if (pinned && run.sessionId !== undefined && run.sessionId !== target?.sessionId) {
201
+ throw new AutomationError('target_resume_failed', 'run target snapshot does not match its session id.');
202
+ }
203
+ const sessionId = SessionId(target !== undefined ? target.sessionId : (run.sessionId ?? `automation-${randomUUID()}`));
204
+ let handle;
205
+ let agent;
206
+ let keepSessionLive = false;
207
+ this.active.set(run.id, active);
208
+ try {
209
+ const canceledDuringValidation = new Promise((_resolve, reject) => {
210
+ active.cancelValidation = () => reject(new Error(`Automation run canceled before Agent creation: ${active.cancelReason}.`));
211
+ });
212
+ const validationExecution = pinned
213
+ ? { ...task.execution, agentPreset: undefined, provider: undefined, model: undefined, reasoningEffort: undefined, skills: [] }
214
+ : task.execution;
215
+ await Promise.race([
216
+ this.agentConfiguration.validate(validationExecution, task.security.permissionPreset, { allowLegacyPartialModel: true }),
217
+ canceledDuringValidation,
218
+ ]);
219
+ const workspace = this.ctx.workspaceRegistry.get(WorkspaceId(target?.workspaceId ?? task.execution.workspaceId))
220
+ ?? (pinned ? undefined : await this.ctx.workspaceRegistry.create(task.execution.cwd));
221
+ if (workspace === undefined)
222
+ throw new AutomationError('target_workspace_mismatch', 'target workspace is unavailable.');
223
+ if (target !== undefined && workspace.path !== target.cwd)
224
+ throw new AutomationError('target_workspace_mismatch', 'target cwd does not match.');
225
+ if (pinned) {
226
+ const persistence = this.ctx.get('sessionPersistence');
227
+ if (persistence === undefined)
228
+ throw new AutomationError('target_session_unavailable', 'persisted session inspection is unavailable.');
229
+ const snapshot = await persistence.stat(sessionId);
230
+ if (snapshot === undefined || snapshot.header.id !== sessionId)
231
+ throw new AutomationError('target_session_not_found', 'pinned session could not be resolved.');
232
+ if (snapshot.header.cwd !== target?.cwd)
233
+ throw new AutomationError('target_workspace_mismatch', 'target session cwd does not match.');
234
+ agent = this.ctx.agents.get(sessionId);
235
+ if (agent === undefined && active.cancelReason === undefined) {
236
+ // Bare agents.resume does not restore the Session's preset or model
237
+ // selection. Let the same owner used by Web restore and retain it.
238
+ const controller = this.ctx.get('sessionController');
239
+ if (controller === undefined)
240
+ throw new AutomationError('target_session_unavailable', 'Host session controller is unavailable.');
241
+ const resolved = await controller.resolveAgent(sessionId);
242
+ if ('error' in resolved)
243
+ throw new AutomationError('target_resume_failed', resolved.error.message);
244
+ agent = resolved.agent;
245
+ }
246
+ }
247
+ else {
248
+ handle = await this.ctx.agents.create({
249
+ sessionId,
250
+ meta: { cwd: workspace.path, ...(task.execution.agentPreset === undefined ? {} : { agentPreset: task.execution.agentPreset }) },
251
+ agentOptions: {
252
+ ...(task.execution.provider === undefined ? {} : { provider: task.execution.provider }),
253
+ ...(task.execution.model === undefined ? {} : { model: task.execution.model }),
254
+ ...(task.execution.reasoningEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(task.execution.reasoningEffort) }),
255
+ },
256
+ setup: async (agentCtx) => { await this.ctx.agentPresets.mount(agentCtx, task.execution.agentPreset); },
257
+ });
258
+ agent = handle.agent;
259
+ }
260
+ if (active.cancelReason !== undefined) {
261
+ keepSessionLive = !pinned;
262
+ return { status: 'failed', sessionId, error: `Automation run canceled before execution: ${active.cancelReason}.` };
263
+ }
264
+ if (agent === undefined)
265
+ throw new AutomationError('target_resume_failed', 'unable to create agent runtime.');
266
+ if (pinned && agent.session.header.id !== sessionId)
267
+ throw new AutomationError('target_session_not_found', 'resumed session id does not match target.');
268
+ if (pinned && agent.session.header.cwd !== target?.cwd)
269
+ throw new AutomationError('target_workspace_mismatch', 'resumed session cwd does not match.');
270
+ if (pinned && (agent.status !== 'idle' || unattendedAgents.has(agent)))
271
+ throw new AutomationError('target_session_busy', 'target session is busy.');
272
+ const selectedSkills = pinned ? [] : await Promise.race([
273
+ this.agentConfiguration.loadSelectedSkills(agent, task), canceledDuringValidation,
274
+ ]);
275
+ if (!pinned) {
276
+ this.ctx.sessionTitle.rename(agent.session, titleFor(task, run));
277
+ await this.ctx.sessions.flush(agent.session);
278
+ await workspace.attachSession(sessionId);
279
+ }
280
+ // Once the session can accept shared input, retiring this run must not
281
+ // dispose it and abort a queued human turn. Its owning fiber retains it.
282
+ keepSessionLive = true;
283
+ const result = await this.runTurn(agent, task, run, active, selectedSkills);
284
+ await this.ctx.sessions.flush(agent.session);
285
+ return result;
286
+ }
287
+ finally {
288
+ delete active.cancelValidation;
289
+ this.active.delete(run.id);
290
+ if (handle !== undefined && !keepSessionLive)
291
+ await handle.dispose();
292
+ }
293
+ }
294
+ }
295
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,3 @@
1
+ import type { Agent } from '@deepseek-ai/dsh-agent';
2
+ export declare const unattendedAgents: WeakSet<Agent>;
3
+ //# sourceMappingURL=runtime-marker.d.ts.map
@@ -0,0 +1,2 @@
1
+ export const unattendedAgents = new WeakSet();
2
+ //# sourceMappingURL=runtime-marker.js.map