mcp-codex-worker 0.1.11 → 0.1.13

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 (73) hide show
  1. package/dist/src/execution/base-adapter.d.ts +63 -0
  2. package/dist/src/execution/base-adapter.js +74 -0
  3. package/dist/src/execution/base-adapter.js.map +1 -0
  4. package/dist/src/execution/claude-adapter.d.ts +12 -0
  5. package/dist/src/execution/claude-adapter.js +23 -0
  6. package/dist/src/execution/claude-adapter.js.map +1 -0
  7. package/dist/src/execution/codex-adapter.d.ts +21 -0
  8. package/dist/src/execution/codex-adapter.js +35 -0
  9. package/dist/src/execution/codex-adapter.js.map +1 -0
  10. package/dist/src/execution/codex-pause-flow.d.ts +23 -0
  11. package/dist/src/execution/codex-pause-flow.js +185 -0
  12. package/dist/src/execution/codex-pause-flow.js.map +1 -0
  13. package/dist/src/execution/copilot-adapter.d.ts +12 -0
  14. package/dist/src/execution/copilot-adapter.js +23 -0
  15. package/dist/src/execution/copilot-adapter.js.map +1 -0
  16. package/dist/src/execution/provider-capabilities.d.ts +35 -0
  17. package/dist/src/execution/provider-capabilities.js +58 -0
  18. package/dist/src/execution/provider-capabilities.js.map +1 -0
  19. package/dist/src/execution/provider-registry.d.ts +37 -0
  20. package/dist/src/execution/provider-registry.js +69 -0
  21. package/dist/src/execution/provider-registry.js.map +1 -0
  22. package/dist/src/mcp/resource-renderers.d.ts +25 -0
  23. package/dist/src/mcp/resource-renderers.js +190 -0
  24. package/dist/src/mcp/resource-renderers.js.map +1 -0
  25. package/dist/src/mcp/sep1686-handlers.d.ts +56 -0
  26. package/dist/src/mcp/sep1686-handlers.js +98 -0
  27. package/dist/src/mcp/sep1686-handlers.js.map +1 -0
  28. package/dist/src/mcp/tool-definitions.d.ts +157 -0
  29. package/dist/src/mcp/tool-definitions.js +242 -0
  30. package/dist/src/mcp/tool-definitions.js.map +1 -1
  31. package/dist/src/task/fsm-transitions.d.ts +17 -0
  32. package/dist/src/task/fsm-transitions.js +66 -0
  33. package/dist/src/task/fsm-transitions.js.map +1 -0
  34. package/dist/src/task/task-handle-impl.d.ts +10 -0
  35. package/dist/src/task/task-handle-impl.js +139 -0
  36. package/dist/src/task/task-handle-impl.js.map +1 -0
  37. package/dist/src/task/task-handle.d.ts +88 -0
  38. package/dist/src/task/task-handle.js +2 -0
  39. package/dist/src/task/task-handle.js.map +1 -0
  40. package/dist/src/task/task-manager.d.ts +99 -0
  41. package/dist/src/task/task-manager.js +246 -0
  42. package/dist/src/task/task-manager.js.map +1 -0
  43. package/dist/src/task/task-persistence.d.ts +18 -0
  44. package/dist/src/task/task-persistence.js +61 -0
  45. package/dist/src/task/task-persistence.js.map +1 -0
  46. package/dist/src/task/task-state.d.ts +79 -0
  47. package/dist/src/task/task-state.js +24 -0
  48. package/dist/src/task/task-state.js.map +1 -0
  49. package/dist/src/task/task-store.d.ts +46 -0
  50. package/dist/src/task/task-store.js +104 -0
  51. package/dist/src/task/task-store.js.map +1 -0
  52. package/dist/src/task/wire-state-mapper.d.ts +21 -0
  53. package/dist/src/task/wire-state-mapper.js +63 -0
  54. package/dist/src/task/wire-state-mapper.js.map +1 -0
  55. package/package.json +2 -1
  56. package/src/execution/base-adapter.ts +133 -0
  57. package/src/execution/claude-adapter.ts +40 -0
  58. package/src/execution/codex-adapter.ts +67 -0
  59. package/src/execution/codex-pause-flow.ts +225 -0
  60. package/src/execution/copilot-adapter.ts +40 -0
  61. package/src/execution/provider-capabilities.ts +100 -0
  62. package/src/execution/provider-registry.ts +81 -0
  63. package/src/mcp/resource-renderers.ts +224 -0
  64. package/src/mcp/sep1686-handlers.ts +149 -0
  65. package/src/mcp/tool-definitions.ts +255 -0
  66. package/src/task/fsm-transitions.ts +72 -0
  67. package/src/task/task-handle-impl.ts +170 -0
  68. package/src/task/task-handle.ts +135 -0
  69. package/src/task/task-manager.ts +328 -0
  70. package/src/task/task-persistence.ts +95 -0
  71. package/src/task/task-state.ts +121 -0
  72. package/src/task/task-store.ts +121 -0
  73. package/src/task/wire-state-mapper.ts +77 -0
@@ -0,0 +1,40 @@
1
+ // ---------------------------------------------------------------------------
2
+ // CopilotAdapter — Phase 2 stub
3
+ // Spec reference: §3.4
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { Provider } from '../task/task-state.js';
7
+ import type { TaskHandle } from '../task/task-handle.js';
8
+ import type { ProviderCapabilities } from './provider-capabilities.js';
9
+ import { COPILOT_CAPABILITIES } from './provider-capabilities.js';
10
+ import {
11
+ BaseProviderAdapter,
12
+ type ProviderSpawnOptions,
13
+ type AvailabilityResult,
14
+ } from './base-adapter.js';
15
+
16
+ export class CopilotAdapter extends BaseProviderAdapter {
17
+ readonly id: Provider = 'copilot';
18
+ readonly displayName = 'Copilot';
19
+
20
+ checkAvailability(): AvailabilityResult {
21
+ return { available: false, reason: 'Phase 2 — not yet implemented' };
22
+ }
23
+
24
+ getCapabilities(): ProviderCapabilities {
25
+ return COPILOT_CAPABILITIES;
26
+ }
27
+
28
+ getStats(): Record<string, unknown> {
29
+ return {};
30
+ }
31
+
32
+ protected async executeSession(
33
+ _handle: TaskHandle,
34
+ _prompt: string,
35
+ _signal: AbortSignal,
36
+ _options: ProviderSpawnOptions,
37
+ ): Promise<void> {
38
+ throw new Error('Not implemented — Phase 2');
39
+ }
40
+ }
@@ -0,0 +1,100 @@
1
+ // ---------------------------------------------------------------------------
2
+ // CapabilityMatrix — runtime-enforced provider contract
3
+ // Spec reference: §3.4
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { PendingQuestionType } from '../task/task-state.js';
7
+
8
+ /**
9
+ * Declares what a provider adapter can do at runtime.
10
+ * TaskManager reads these at startup and uses them to route behavior.
11
+ */
12
+ export interface ProviderCapabilities {
13
+ /** Session survives server restart (e.g. Codex rollout files). */
14
+ sessionPersistence: boolean;
15
+ /** Distinct approve/decline protocol (e.g. Codex 4 approval types). */
16
+ structuredApproval: boolean;
17
+ /** Can stream tokens/chunks. */
18
+ streamingOutput: boolean;
19
+ /** Has explicit cancel primitive (e.g. turn/interrupt). */
20
+ abortPrimitive: boolean;
21
+ /** Supports thread/read-style probe for crash recovery. */
22
+ crashRecovery: boolean;
23
+ /** How the provider selects a model. */
24
+ modelRouting: 'config' | 'api-param' | 'none';
25
+ /** Emits structured rate-limit error. */
26
+ rateLimitSignal: boolean;
27
+ /** Subset of the 5 PendingQuestion types this provider supports. */
28
+ pauseTypes: readonly PendingQuestionType[];
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Concrete capability declarations — Phase 1 matrix
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const ALL_PAUSE_TYPES: readonly PendingQuestionType[] = [
36
+ 'user_input',
37
+ 'command_approval',
38
+ 'file_approval',
39
+ 'elicitation',
40
+ 'dynamic_tool',
41
+ ] as const;
42
+
43
+ export const CODEX_CAPABILITIES: Readonly<ProviderCapabilities> = {
44
+ sessionPersistence: true,
45
+ structuredApproval: true,
46
+ streamingOutput: true,
47
+ abortPrimitive: true,
48
+ crashRecovery: true,
49
+ modelRouting: 'config',
50
+ rateLimitSignal: true,
51
+ pauseTypes: ALL_PAUSE_TYPES,
52
+ };
53
+
54
+ export const COPILOT_CAPABILITIES: Readonly<ProviderCapabilities> = {
55
+ sessionPersistence: false,
56
+ structuredApproval: false,
57
+ streamingOutput: true,
58
+ abortPrimitive: true,
59
+ crashRecovery: false,
60
+ modelRouting: 'api-param',
61
+ rateLimitSignal: true,
62
+ pauseTypes: ['user_input'] as const,
63
+ };
64
+
65
+ export const CLAUDE_CAPABILITIES: Readonly<ProviderCapabilities> = {
66
+ sessionPersistence: false,
67
+ structuredApproval: false,
68
+ streamingOutput: true,
69
+ abortPrimitive: true,
70
+ crashRecovery: false,
71
+ modelRouting: 'api-param',
72
+ rateLimitSignal: true,
73
+ pauseTypes: ['user_input'] as const,
74
+ };
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Validation
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export interface ValidationResult {
81
+ ok: boolean;
82
+ reason?: string;
83
+ }
84
+
85
+ /**
86
+ * Validates that a pause type is supported by the provider's capabilities.
87
+ * Returns `{ ok: false, reason }` if the provider does not support the given type.
88
+ */
89
+ export function validateResponseAgainstCapabilities(
90
+ caps: ProviderCapabilities,
91
+ pauseType: PendingQuestionType,
92
+ ): ValidationResult {
93
+ if (caps.pauseTypes.includes(pauseType)) {
94
+ return { ok: true };
95
+ }
96
+ return {
97
+ ok: false,
98
+ reason: `this provider does not support ${pauseType} responses`,
99
+ };
100
+ }
@@ -0,0 +1,81 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ProviderRegistry — routes task types to provider adapters
3
+ // Spec reference: §3.4
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import type { Provider, TaskTypeName } from '../task/task-state.js';
7
+ import type { BaseProviderAdapter } from './base-adapter.js';
8
+
9
+ /**
10
+ * Central registry for provider adapters.
11
+ *
12
+ * Supports:
13
+ * - Registration by provider ID
14
+ * - Default fallback provider
15
+ * - Per-task-type routing overrides
16
+ * - Bulk shutdown
17
+ */
18
+ export class ProviderRegistry {
19
+ private readonly adapters = new Map<Provider, BaseProviderAdapter>();
20
+ private defaultProvider: Provider | undefined;
21
+ private readonly taskTypeRoutes = new Map<TaskTypeName, Provider>();
22
+
23
+ /** Register an adapter. Keyed by its `id` property. */
24
+ register(adapter: BaseProviderAdapter): void {
25
+ this.adapters.set(adapter.id, adapter);
26
+ }
27
+
28
+ /** Set the fallback provider used when no task-type route matches. */
29
+ setDefault(provider: Provider): void {
30
+ this.defaultProvider = provider;
31
+ }
32
+
33
+ /** Override routing for a specific task type. */
34
+ setTaskTypeRoute(taskType: TaskTypeName, provider: Provider): void {
35
+ this.taskTypeRoutes.set(taskType, provider);
36
+ }
37
+
38
+ /** Look up an adapter by provider ID. */
39
+ getAdapter(provider: Provider): BaseProviderAdapter | undefined {
40
+ return this.adapters.get(provider);
41
+ }
42
+
43
+ /**
44
+ * Select the best adapter for a given task type.
45
+ *
46
+ * Resolution order:
47
+ * 1. Explicit task-type route
48
+ * 2. Default provider
49
+ * 3. First registered adapter (arbitrary but deterministic)
50
+ */
51
+ selectForTaskType(taskType: TaskTypeName): BaseProviderAdapter | undefined {
52
+ // 1. Check explicit route
53
+ const routed = this.taskTypeRoutes.get(taskType);
54
+ if (routed !== undefined) {
55
+ const adapter = this.adapters.get(routed);
56
+ if (adapter) return adapter;
57
+ }
58
+
59
+ // 2. Check default
60
+ if (this.defaultProvider !== undefined) {
61
+ const adapter = this.adapters.get(this.defaultProvider);
62
+ if (adapter) return adapter;
63
+ }
64
+
65
+ // 3. Fall back to first registered
66
+ const all = Array.from(this.adapters.values());
67
+ return all.length > 0 ? all[0] : undefined;
68
+ }
69
+
70
+ /** Return all registered adapters. */
71
+ getAllAdapters(): BaseProviderAdapter[] {
72
+ return Array.from(this.adapters.values());
73
+ }
74
+
75
+ /** Shut down all registered adapters in parallel. */
76
+ async shutdownAll(): Promise<void> {
77
+ await Promise.all(
78
+ Array.from(this.adapters.values()).map((a) => a.shutdown()),
79
+ );
80
+ }
81
+ }
@@ -0,0 +1,224 @@
1
+ import { TaskStatus, TERMINAL_STATUSES } from '../task/task-state.js';
2
+ import type { TaskState, PendingQuestion } from '../task/task-state.js';
3
+ import { mapToDisplay } from '../task/wire-state-mapper.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Constants
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const PROMPT_MAX_LEN = 50;
10
+ const SUMMARY_LOG_LINES = 20;
11
+ const DETAIL_OUTPUT_LINES = 10;
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Sorting helpers
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Active statuses that should appear first on the scoreboard. */
18
+ const ACTIVE_STATUSES: ReadonlySet<TaskStatus> = new Set([
19
+ TaskStatus.RUNNING,
20
+ TaskStatus.WAITING_ANSWER,
21
+ TaskStatus.RATE_LIMITED,
22
+ ]);
23
+
24
+ /** Pending statuses that appear between active and terminal. */
25
+ const PENDING_STATUSES: ReadonlySet<TaskStatus> = new Set([
26
+ TaskStatus.WAITING,
27
+ TaskStatus.PENDING,
28
+ ]);
29
+
30
+ function sortPriority(status: TaskStatus): number {
31
+ if (ACTIVE_STATUSES.has(status)) return 0;
32
+ if (PENDING_STATUSES.has(status)) return 1;
33
+ return 2; // terminal
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Formatting helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ function truncatePrompt(prompt: string, maxLen: number = PROMPT_MAX_LEN): string {
41
+ if (prompt.length <= maxLen) return prompt;
42
+ return prompt.slice(0, maxLen - 3) + '...';
43
+ }
44
+
45
+ function formatElapsed(createdAt: string, updatedAt: string): string {
46
+ const start = new Date(createdAt).getTime();
47
+ const end = new Date(updatedAt).getTime();
48
+ const diffMs = Math.max(0, end - start);
49
+
50
+ const totalSec = Math.floor(diffMs / 1000);
51
+ const min = Math.floor(totalSec / 60);
52
+ const sec = totalSec % 60;
53
+
54
+ if (min > 0) {
55
+ return `${min}m ${sec}s`;
56
+ }
57
+ return `${sec}s`;
58
+ }
59
+
60
+ /**
61
+ * Build a status-count summary string like "3 done, 1 busy, 1 wait".
62
+ * Uses the display badge labels (without brackets) as category names.
63
+ */
64
+ function statusSummary(tasks: TaskState[]): string {
65
+ const counts = new Map<string, number>();
66
+ for (const t of tasks) {
67
+ const badge = mapToDisplay(t.status);
68
+ // Strip brackets: "[busy]" → "busy"
69
+ const label = badge.slice(1, -1);
70
+ counts.set(label, (counts.get(label) ?? 0) + 1);
71
+ }
72
+ return Array.from(counts.entries())
73
+ .map(([label, count]) => `${count} ${label}`)
74
+ .join(', ');
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // renderScoreboard
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Compact all-tasks view.
83
+ *
84
+ * Resource URI: `task:///all`
85
+ */
86
+ export function renderScoreboard(tasks: TaskState[]): string {
87
+ const sorted = [...tasks].sort((a, b) => sortPriority(a.status) - sortPriority(b.status));
88
+
89
+ const summary = tasks.length > 0 ? ` (${statusSummary(tasks)})` : '';
90
+ const header = `tasks -- ${tasks.length} total${summary}`;
91
+
92
+ const lines: string[] = [header, ''];
93
+
94
+ for (const task of sorted) {
95
+ const badge = mapToDisplay(task.status);
96
+ const prompt = truncatePrompt(task.prompt);
97
+ const elapsed = formatElapsed(task.createdAt, task.updatedAt);
98
+ lines.push(`${badge} ${task.id} -- "${prompt}" (${elapsed})`);
99
+ }
100
+
101
+ return lines.join('\n');
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // renderTaskDetail
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Full task detail view as markdown.
110
+ *
111
+ * Resource URI: `task:///{id}`
112
+ */
113
+ export function renderTaskDetail(task: TaskState): string {
114
+ const badge = mapToDisplay(task.status);
115
+
116
+ const lines: string[] = [
117
+ `# Task: ${task.id} -- ${truncatePrompt(task.prompt)}`,
118
+ '',
119
+ '| Field | Value |',
120
+ '|---|---|',
121
+ `| **Status** | ${badge} \`${task.status}\` |`,
122
+ `| **Provider** | ${task.provider} |`,
123
+ ];
124
+
125
+ if (task.sessionId) {
126
+ lines.push(`| **Session ID** | \`${task.sessionId}\` |`);
127
+ }
128
+
129
+ if (task.model) {
130
+ lines.push(`| **Model** | \`${task.model}\` |`);
131
+ }
132
+
133
+ lines.push(`| **Task type** | ${task.taskType} |`);
134
+ lines.push(`| **CWD** | \`${task.cwd}\` |`);
135
+ lines.push(`| **Created** | ${task.createdAt} |`);
136
+
137
+ if (task.startedAt) {
138
+ lines.push(`| **Started** | ${task.startedAt} |`);
139
+ }
140
+ if (task.completedAt) {
141
+ lines.push(`| **Completed** | ${task.completedAt} |`);
142
+ }
143
+
144
+ lines.push(`| **Updated** | ${task.updatedAt} |`);
145
+ lines.push('');
146
+
147
+ // Pending questions
148
+ if (task.pendingQuestions.length > 0) {
149
+ lines.push('## Pending Question', '');
150
+ for (const q of task.pendingQuestions) {
151
+ lines.push(formatPendingQuestion(q));
152
+ }
153
+ lines.push('');
154
+ }
155
+
156
+ // Error
157
+ if (task.error) {
158
+ lines.push('## Error', '', `\`\`\`\n${task.error}\n\`\`\``, '');
159
+ }
160
+
161
+ // Last N output lines
162
+ if (task.output.length > 0) {
163
+ const tail = task.output.slice(-DETAIL_OUTPUT_LINES);
164
+ lines.push('## Recent Output', '');
165
+ for (const line of tail) {
166
+ lines.push(line);
167
+ }
168
+ lines.push('');
169
+ }
170
+
171
+ return lines.join('\n');
172
+ }
173
+
174
+ function formatPendingQuestion(q: PendingQuestion): string {
175
+ switch (q.type) {
176
+ case 'user_input':
177
+ return q.questions.map((iq) => `- **${iq.text}**${iq.options ? ` (options: ${iq.options.join(', ')})` : ''}`).join('\n');
178
+ case 'command_approval':
179
+ return `- **Command approval**: \`${q.command}\``;
180
+ case 'file_approval':
181
+ return `- **File approval**: ${q.fileChanges.map((f) => f.path).join(', ')}`;
182
+ case 'elicitation':
183
+ return `- **Elicitation**: ${q.message}`;
184
+ case 'dynamic_tool':
185
+ return `- **Dynamic tool**: \`${q.toolName}\``;
186
+ }
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // renderSummaryLog
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Last 20 lines from task.output.
195
+ *
196
+ * Resource URI: `task:///{id}/log`
197
+ */
198
+ export function renderSummaryLog(task: TaskState): string {
199
+ if (task.output.length === 0) {
200
+ return `# Log: ${task.id}\n\nNo output yet.`;
201
+ }
202
+
203
+ const tail = task.output.slice(-SUMMARY_LOG_LINES);
204
+ const header = `# Log: ${task.id} (last ${tail.length} of ${task.output.length} lines)`;
205
+ return [header, '', ...tail].join('\n');
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // renderVerboseLog
210
+ // ---------------------------------------------------------------------------
211
+
212
+ /**
213
+ * All lines from task.output (no truncation).
214
+ *
215
+ * Resource URI: `task:///{id}/log.verbose`
216
+ */
217
+ export function renderVerboseLog(task: TaskState): string {
218
+ if (task.output.length === 0) {
219
+ return `# Verbose Log: ${task.id}\n\nNo output yet.`;
220
+ }
221
+
222
+ const header = `# Verbose Log: ${task.id} (${task.output.length} lines)`;
223
+ return [header, '', ...task.output].join('\n');
224
+ }
@@ -0,0 +1,149 @@
1
+ import type { TaskState, PendingQuestion } from '../task/task-state.js';
2
+ import { TaskStatus } from '../task/task-state.js';
3
+ import type { WireState } from '../task/wire-state-mapper.js';
4
+ import { mapToWireState } from '../task/wire-state-mapper.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Response interfaces — SEP-1686 wire-compatible shapes
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /**
11
+ * Response shape for `tasks/get` — a single task's status snapshot.
12
+ */
13
+ export interface TasksGetResponse {
14
+ taskId: string;
15
+ status: WireState;
16
+ pollFrequency: number;
17
+ keepAlive?: number;
18
+ metadata?: { pendingQuestion?: PendingQuestion };
19
+ error?: string;
20
+ }
21
+
22
+ /**
23
+ * List item shape for `tasks/list` — lightweight summary per task.
24
+ */
25
+ export interface TasksListItem {
26
+ taskId: string;
27
+ status: WireState;
28
+ pollFrequency: number;
29
+ createdAt: string;
30
+ lastUpdatedAt: string;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Poll frequency computation
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Returns the recommended poll interval in milliseconds based on task status.
39
+ *
40
+ * - WAITING_ANSWER → 2 000 ms (client should check frequently for input-required)
41
+ * - PENDING, RUNNING → 5 000 ms
42
+ * - All others (WAITING, RATE_LIMITED, terminal) → 30 000 ms
43
+ */
44
+ export function computePollFrequency(task: TaskState): number {
45
+ switch (task.status) {
46
+ case TaskStatus.WAITING_ANSWER:
47
+ return 2000;
48
+ case TaskStatus.PENDING:
49
+ case TaskStatus.RUNNING:
50
+ return 5000;
51
+ default:
52
+ return 30000;
53
+ }
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // tasks/get response builder
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Builds a SEP-1686 `tasks/get` response from internal TaskState.
62
+ *
63
+ * - Maps internal status to wire state via `mapToWireState`
64
+ * - Computes poll frequency via `computePollFrequency`
65
+ * - Includes `keepAlive` from task if present
66
+ * - Includes first pending question in metadata when status is
67
+ * WAITING_ANSWER and the queue is non-empty
68
+ * - Includes error string if present
69
+ */
70
+ export function buildTasksGetResponse(task: TaskState): TasksGetResponse {
71
+ const status = mapToWireState(task.status);
72
+ const pollFrequency = computePollFrequency(task);
73
+
74
+ const hasPendingQuestion =
75
+ task.status === TaskStatus.WAITING_ANSWER &&
76
+ task.pendingQuestions.length > 0;
77
+
78
+ const response: TasksGetResponse = {
79
+ taskId: task.id,
80
+ status,
81
+ pollFrequency,
82
+ };
83
+
84
+ if (task.keepAlive !== undefined) {
85
+ response.keepAlive = task.keepAlive;
86
+ }
87
+
88
+ if (hasPendingQuestion) {
89
+ const firstQuestion = task.pendingQuestions[0];
90
+ if (firstQuestion !== undefined) {
91
+ response.metadata = { pendingQuestion: firstQuestion };
92
+ }
93
+ }
94
+
95
+ if (task.error !== undefined) {
96
+ response.error = task.error;
97
+ }
98
+
99
+ return response;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // tasks/list response builder
104
+ // ---------------------------------------------------------------------------
105
+
106
+ const PAGE_SIZE = 50;
107
+
108
+ /**
109
+ * Builds a SEP-1686 `tasks/list` response.
110
+ *
111
+ * - Filters by wire status if `statusFilter` is provided
112
+ * - Sorts by `createdAt` descending (newest first)
113
+ * - Paginates with page size 50; cursor is a string-encoded start index
114
+ * - Returns `nextCursor` if more pages exist
115
+ */
116
+ export function buildTasksListResponse(
117
+ tasks: TaskState[],
118
+ cursor?: string,
119
+ statusFilter?: WireState,
120
+ ): { tasks: TasksListItem[]; nextCursor?: string } {
121
+ // 1. Filter by wire status if requested
122
+ const filtered = statusFilter
123
+ ? tasks.filter(t => mapToWireState(t.status) === statusFilter)
124
+ : [...tasks];
125
+
126
+ // 2. Sort by createdAt descending
127
+ filtered.sort(
128
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
129
+ );
130
+
131
+ // 3. Paginate
132
+ const start = cursor ? parseInt(cursor, 10) : 0;
133
+ const page = filtered.slice(start, start + PAGE_SIZE);
134
+ const nextStart = start + PAGE_SIZE;
135
+ // 4. Map to list items
136
+ const items: TasksListItem[] = page.map(t => ({
137
+ taskId: t.id,
138
+ status: mapToWireState(t.status),
139
+ pollFrequency: computePollFrequency(t),
140
+ createdAt: t.createdAt,
141
+ lastUpdatedAt: t.updatedAt,
142
+ }));
143
+
144
+ const result: { tasks: TasksListItem[]; nextCursor?: string } = { tasks: items };
145
+ if (nextStart < filtered.length) {
146
+ result.nextCursor = String(nextStart);
147
+ }
148
+ return result;
149
+ }