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.
- package/dist/src/execution/base-adapter.d.ts +63 -0
- package/dist/src/execution/base-adapter.js +74 -0
- package/dist/src/execution/base-adapter.js.map +1 -0
- package/dist/src/execution/claude-adapter.d.ts +12 -0
- package/dist/src/execution/claude-adapter.js +23 -0
- package/dist/src/execution/claude-adapter.js.map +1 -0
- package/dist/src/execution/codex-adapter.d.ts +21 -0
- package/dist/src/execution/codex-adapter.js +35 -0
- package/dist/src/execution/codex-adapter.js.map +1 -0
- package/dist/src/execution/codex-pause-flow.d.ts +23 -0
- package/dist/src/execution/codex-pause-flow.js +185 -0
- package/dist/src/execution/codex-pause-flow.js.map +1 -0
- package/dist/src/execution/copilot-adapter.d.ts +12 -0
- package/dist/src/execution/copilot-adapter.js +23 -0
- package/dist/src/execution/copilot-adapter.js.map +1 -0
- package/dist/src/execution/provider-capabilities.d.ts +35 -0
- package/dist/src/execution/provider-capabilities.js +58 -0
- package/dist/src/execution/provider-capabilities.js.map +1 -0
- package/dist/src/execution/provider-registry.d.ts +37 -0
- package/dist/src/execution/provider-registry.js +69 -0
- package/dist/src/execution/provider-registry.js.map +1 -0
- package/dist/src/mcp/resource-renderers.d.ts +25 -0
- package/dist/src/mcp/resource-renderers.js +190 -0
- package/dist/src/mcp/resource-renderers.js.map +1 -0
- package/dist/src/mcp/sep1686-handlers.d.ts +56 -0
- package/dist/src/mcp/sep1686-handlers.js +98 -0
- package/dist/src/mcp/sep1686-handlers.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +157 -0
- package/dist/src/mcp/tool-definitions.js +242 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -1
- package/dist/src/task/fsm-transitions.d.ts +17 -0
- package/dist/src/task/fsm-transitions.js +66 -0
- package/dist/src/task/fsm-transitions.js.map +1 -0
- package/dist/src/task/task-handle-impl.d.ts +10 -0
- package/dist/src/task/task-handle-impl.js +139 -0
- package/dist/src/task/task-handle-impl.js.map +1 -0
- package/dist/src/task/task-handle.d.ts +88 -0
- package/dist/src/task/task-handle.js +2 -0
- package/dist/src/task/task-handle.js.map +1 -0
- package/dist/src/task/task-manager.d.ts +99 -0
- package/dist/src/task/task-manager.js +246 -0
- package/dist/src/task/task-manager.js.map +1 -0
- package/dist/src/task/task-persistence.d.ts +18 -0
- package/dist/src/task/task-persistence.js +61 -0
- package/dist/src/task/task-persistence.js.map +1 -0
- package/dist/src/task/task-state.d.ts +79 -0
- package/dist/src/task/task-state.js +24 -0
- package/dist/src/task/task-state.js.map +1 -0
- package/dist/src/task/task-store.d.ts +46 -0
- package/dist/src/task/task-store.js +104 -0
- package/dist/src/task/task-store.js.map +1 -0
- package/dist/src/task/wire-state-mapper.d.ts +21 -0
- package/dist/src/task/wire-state-mapper.js +63 -0
- package/dist/src/task/wire-state-mapper.js.map +1 -0
- package/package.json +2 -1
- package/src/execution/base-adapter.ts +133 -0
- package/src/execution/claude-adapter.ts +40 -0
- package/src/execution/codex-adapter.ts +67 -0
- package/src/execution/codex-pause-flow.ts +225 -0
- package/src/execution/copilot-adapter.ts +40 -0
- package/src/execution/provider-capabilities.ts +100 -0
- package/src/execution/provider-registry.ts +81 -0
- package/src/mcp/resource-renderers.ts +224 -0
- package/src/mcp/sep1686-handlers.ts +149 -0
- package/src/mcp/tool-definitions.ts +255 -0
- package/src/task/fsm-transitions.ts +72 -0
- package/src/task/task-handle-impl.ts +170 -0
- package/src/task/task-handle.ts +135 -0
- package/src/task/task-manager.ts +328 -0
- package/src/task/task-persistence.ts +95 -0
- package/src/task/task-state.ts +121 -0
- package/src/task/task-store.ts +121 -0
- 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
|
+
}
|