@zq-silk/yui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +141 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/agent/adapterCatalog.js +10 -0
- package/dist/agent/agent.js +89 -0
- package/dist/agent/agentRegistry.js +10 -0
- package/dist/agent/argumentPolicy.js +80 -0
- package/dist/brief/taskBrief.js +37 -0
- package/dist/cli/commandCatalog.js +647 -0
- package/dist/cli/completion.js +111 -0
- package/dist/cli/completionWizard.js +143 -0
- package/dist/cli/dynamicCompletion.js +48 -0
- package/dist/cli/helpRenderer.js +32 -0
- package/dist/cli/interactionCandidates.js +139 -0
- package/dist/cli/interactionPolicy.js +389 -0
- package/dist/cli/interactiveSelection.js +185 -0
- package/dist/cli/invocationRouter.js +51 -0
- package/dist/cli/roleOptionCatalog.js +67 -0
- package/dist/cli/roleWizard.js +546 -0
- package/dist/cli/selectionPorts.js +1 -0
- package/dist/cli/updateCommand.js +22 -0
- package/dist/cli.js +402 -0
- package/dist/commands/agentCommands.js +196 -0
- package/dist/commands/globalRoleCommands.js +367 -0
- package/dist/commands/jobCommands.js +100 -0
- package/dist/commands/operatorCommands.js +38 -0
- package/dist/commands/repositoryCommands.js +86 -0
- package/dist/commands/roleConfiguration.js +201 -0
- package/dist/commands/taskCommands.js +1344 -0
- package/dist/commands/taskContextCommand.js +215 -0
- package/dist/commands/taskInputCommands.js +423 -0
- package/dist/commands/taskRoleRuntimeStatus.js +152 -0
- package/dist/completion/completionInstaller.js +168 -0
- package/dist/completion/completionPort.js +1 -0
- package/dist/completion/completionState.js +137 -0
- package/dist/completion/completionWizard.js +125 -0
- package/dist/completion/fileCompletionManager.js +51 -0
- package/dist/config/yuiConfig.js +17 -0
- package/dist/context/dispatchContext.js +74 -0
- package/dist/controller/clientRuntime.js +215 -0
- package/dist/controller/controller.js +158 -0
- package/dist/controller/controllerMain.js +37 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
- package/dist/controller/runtime.js +31 -0
- package/dist/controller/sessionNotify.js +136 -0
- package/dist/core/controllerClient.js +127 -0
- package/dist/core/controllerServer.js +269 -0
- package/dist/core/protocol.js +169 -0
- package/dist/decision/decision.js +42 -0
- package/dist/doctor/doctor.js +229 -0
- package/dist/errors/cliError.js +38 -0
- package/dist/event/taskEvent.js +44 -0
- package/dist/executor/agentAdapter.js +338 -0
- package/dist/executor/agentExecutor.js +144 -0
- package/dist/executor/executorRegistry.js +101 -0
- package/dist/executor/fileRoleLaunchPlanner.js +156 -0
- package/dist/executor/launchPlan.js +16 -0
- package/dist/input/inputRequest.js +326 -0
- package/dist/message/message.js +69 -0
- package/dist/milestone/milestone.js +27 -0
- package/dist/operator/operatorContext.js +66 -0
- package/dist/output/rolePresentation.js +82 -0
- package/dist/output/table.js +77 -0
- package/dist/output/terminal.js +198 -0
- package/dist/repository/gitWorkspace.js +210 -0
- package/dist/repository/repository.js +55 -0
- package/dist/repository/taskWorkspacePreparer.js +256 -0
- package/dist/role/role.js +246 -0
- package/dist/role/systemRoles.js +20 -0
- package/dist/run/agentRun.js +102 -0
- package/dist/scheduler/activeRoleRunDelivery.js +94 -0
- package/dist/scheduler/archivedTaskRuntime.js +12 -0
- package/dist/scheduler/leaderFailure.js +18 -0
- package/dist/scheduler/leaderWakeupProcessor.js +143 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
- package/dist/scheduler/operatorNotification.js +17 -0
- package/dist/scheduler/pendingWakeup.js +33 -0
- package/dist/scheduler/ports.js +1 -0
- package/dist/scheduler/roleRunLiveness.js +41 -0
- package/dist/scheduler/wakeupQueue.js +13 -0
- package/dist/setup/setupCommand.js +317 -0
- package/dist/storage/durableFile.js +38 -0
- package/dist/storage/storageSchema.js +259 -0
- package/dist/storage/taskStore.js +1032 -0
- package/dist/task/task.js +216 -0
- package/dist/tmux/commandExecutor.js +69 -0
- package/dist/tmux/terminalHandoff.js +17 -0
- package/dist/tmux/tmuxManager.js +408 -0
- package/dist/workItem/workItem.js +45 -0
- package/dist/worktree/roleWorkspace.js +62 -0
- package/i18n/README.zh-CN.md +205 -0
- package/package.json +47 -0
- package/skills/yui-leader/SKILL.md +72 -0
- package/skills/yui-operator/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +31 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { taskNotFound, usageError } from "../errors/cliError.js";
|
|
2
|
+
import { taskMessageAuthorLabel } from "../message/message.js";
|
|
3
|
+
const RECENT_RECORD_LIMIT = 5;
|
|
4
|
+
const RELATED_RECORD_LIMIT = 5;
|
|
5
|
+
const SUMMARY_TEXT_LIMIT = 400;
|
|
6
|
+
const TERMINAL_WORK_ITEM_STATUSES = new Set([
|
|
7
|
+
"completed",
|
|
8
|
+
"failed",
|
|
9
|
+
"cancelled",
|
|
10
|
+
"superseded"
|
|
11
|
+
]);
|
|
12
|
+
export function runTaskContextCommand(args, store) {
|
|
13
|
+
if (args.length !== 1 || args[0]?.trim().length === 0) {
|
|
14
|
+
throw usageError("Task context usage: yui task context <task>.");
|
|
15
|
+
}
|
|
16
|
+
const taskId = args[0].trim();
|
|
17
|
+
const data = store.transaction((reader) => {
|
|
18
|
+
const task = reader.getTask(taskId);
|
|
19
|
+
if (task === null)
|
|
20
|
+
throw taskNotFound(taskId);
|
|
21
|
+
const workItems = reader.listWorkItems(task.id);
|
|
22
|
+
const workItemIds = new Set(workItems.map((item) => item.id));
|
|
23
|
+
const inputRequests = reader.listInputRequests(task.id);
|
|
24
|
+
return {
|
|
25
|
+
task,
|
|
26
|
+
brief: reader.getTaskBrief(task.id),
|
|
27
|
+
activeDecisions: reader.listDecisions(task.id)
|
|
28
|
+
.filter((decision) => decision.status === "active"),
|
|
29
|
+
milestones: reader.listMilestones(task.id),
|
|
30
|
+
roles: reader.listRoles(task.id),
|
|
31
|
+
workItems,
|
|
32
|
+
runs: reader.listAgentRuns(task.id)
|
|
33
|
+
.filter((run) => run.workItemId !== undefined && workItemIds.has(run.workItemId)),
|
|
34
|
+
messages: reader.listMessages(task.id),
|
|
35
|
+
openInputRequests: inputRequests.filter((request) => request.status === "open"),
|
|
36
|
+
resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
|
|
37
|
+
events: reader.listEvents(task.id)
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const { task, brief, activeDecisions, milestones, roles, workItems, runs, messages, openInputRequests, resolvedInputRequests, events } = data;
|
|
41
|
+
const displayedActiveDecisions = activeDecisions.slice(-RECENT_RECORD_LIMIT);
|
|
42
|
+
const displayedWorkItems = currentAndRecentWorkItems(workItems);
|
|
43
|
+
const displayedOpenInputRequests = openInputRequests.slice(-RECENT_RECORD_LIMIT);
|
|
44
|
+
const displayedResolvedInputRequests = resolvedInputRequests.slice(-RECENT_RECORD_LIMIT);
|
|
45
|
+
const lines = [
|
|
46
|
+
`Task context: ${task.id}`,
|
|
47
|
+
`Title: ${compactText(task.title)}`,
|
|
48
|
+
`Status: ${task.status}`,
|
|
49
|
+
...(task.description === undefined ? [] : [`Description: ${compactText(task.description)}`]),
|
|
50
|
+
...(task.priority === undefined ? [] : [`Priority: ${task.priority}`]),
|
|
51
|
+
...(task.tags === undefined ? [] : [`Tags: ${task.tags.join(", ")}`]),
|
|
52
|
+
...(task.dueAt === undefined ? [] : [`Due: ${task.dueAt}`]),
|
|
53
|
+
...(task.completionSummary === undefined ? [] : [`Completion summary: ${task.completionSummary}`]),
|
|
54
|
+
...(task.archiveSummary === undefined ? [] : [`Archive summary: ${task.archiveSummary}`]),
|
|
55
|
+
...(task.repositoryId === undefined ? [] : [`Repository: ${task.repositoryId}`]),
|
|
56
|
+
...(task.baseRef === undefined ? [] : [`Base: ${task.baseRef}`]),
|
|
57
|
+
...(task.cwd === undefined ? [] : [`Workspace: ${task.cwd}`]),
|
|
58
|
+
"",
|
|
59
|
+
"Brief:",
|
|
60
|
+
...(brief === null
|
|
61
|
+
? [" No brief."]
|
|
62
|
+
: [
|
|
63
|
+
` Objective: ${compactText(brief.objective)}`,
|
|
64
|
+
" Boundaries:",
|
|
65
|
+
...(brief.boundaries.length === 0
|
|
66
|
+
? [" None."]
|
|
67
|
+
: brief.boundaries.map((boundary) => ` - ${compactText(boundary)}`)),
|
|
68
|
+
` Current focus: ${compactText(brief.currentFocus)}`,
|
|
69
|
+
` Leader summary: ${compactText(brief.leaderSummary)}`,
|
|
70
|
+
` Updated by ${brief.updatedBy} at ${brief.updatedAt}`
|
|
71
|
+
]),
|
|
72
|
+
"",
|
|
73
|
+
`Active decisions (${displayedActiveDecisions.length}${activeDecisions.length > displayedActiveDecisions.length ? ` of ${activeDecisions.length}` : ""}):`,
|
|
74
|
+
...(displayedActiveDecisions.length === 0
|
|
75
|
+
? [" None."]
|
|
76
|
+
: displayedActiveDecisions.flatMap((decision) => [
|
|
77
|
+
` ${decision.id}: ${compactText(decision.title)}`,
|
|
78
|
+
` Rationale: ${compactText(decision.rationale)}`
|
|
79
|
+
])),
|
|
80
|
+
"",
|
|
81
|
+
...recentSection("milestones", milestones, (milestone) => [
|
|
82
|
+
` ${milestone.id}: ${compactText(milestone.title)} (${milestone.createdAt})`,
|
|
83
|
+
` ${compactText(milestone.summary)}`
|
|
84
|
+
]),
|
|
85
|
+
"",
|
|
86
|
+
`Roles (${roles.length}):`,
|
|
87
|
+
...(roles.length === 0
|
|
88
|
+
? [" None."]
|
|
89
|
+
: roles.map((role) => (` ${role.name} [${role.status}] — Agent: ${role.activeAgentId}`))),
|
|
90
|
+
"",
|
|
91
|
+
`Current and recent work items (${displayedWorkItems.length}${workItems.length > displayedWorkItems.length ? ` of ${workItems.length}` : ""}):`,
|
|
92
|
+
...(displayedWorkItems.length === 0
|
|
93
|
+
? [" None."]
|
|
94
|
+
: displayedWorkItems.flatMap((item) => {
|
|
95
|
+
const itemRuns = runs.filter((run) => run.workItemId === item.id);
|
|
96
|
+
const latestRun = itemRuns.at(-1);
|
|
97
|
+
return [
|
|
98
|
+
` ${item.id} [${item.status}] ${item.assignee}: ${compactText(item.title)}`,
|
|
99
|
+
...(item.outcome === undefined
|
|
100
|
+
? []
|
|
101
|
+
: [` Outcome: ${compactText(item.outcome)}`]),
|
|
102
|
+
...(latestRun === undefined
|
|
103
|
+
? [" Runs: none."]
|
|
104
|
+
: [
|
|
105
|
+
` Runs: ${itemRuns.length}; latest ${latestRun.id} [${latestRun.status}] ${latestRun.roleName}`,
|
|
106
|
+
` Input: ${compactText(latestRun.input)}`,
|
|
107
|
+
...(latestRun.summary === undefined
|
|
108
|
+
? []
|
|
109
|
+
: [` Summary: ${compactText(latestRun.summary)}`])
|
|
110
|
+
])
|
|
111
|
+
];
|
|
112
|
+
})),
|
|
113
|
+
"",
|
|
114
|
+
...recentSection("messages", messages, (message) => [
|
|
115
|
+
` ${message.id} [${taskMessageAuthorLabel(message.author)}] ${message.createdAt}`,
|
|
116
|
+
` ${compactText(message.body)}`
|
|
117
|
+
]),
|
|
118
|
+
"",
|
|
119
|
+
`Open input requests (${displayedOpenInputRequests.length}${openInputRequests.length > displayedOpenInputRequests.length ? ` of ${openInputRequests.length}` : ""}):`,
|
|
120
|
+
...(displayedOpenInputRequests.length === 0
|
|
121
|
+
? [" None."]
|
|
122
|
+
: displayedOpenInputRequests.flatMap(renderOpenInputRequest)),
|
|
123
|
+
"",
|
|
124
|
+
`Recent resolved input requests (${displayedResolvedInputRequests.length}${resolvedInputRequests.length > displayedResolvedInputRequests.length ? ` of ${resolvedInputRequests.length}` : ""}):`,
|
|
125
|
+
...(displayedResolvedInputRequests.length === 0
|
|
126
|
+
? [" None."]
|
|
127
|
+
: displayedResolvedInputRequests.flatMap(renderResolvedInputRequest)),
|
|
128
|
+
"",
|
|
129
|
+
...recentSection("events", events, (event) => [
|
|
130
|
+
` ${event.id} ${event.type} (${event.createdAt})`,
|
|
131
|
+
...(Object.keys(event.payload).length === 0
|
|
132
|
+
? []
|
|
133
|
+
: [` ${Object.entries(event.payload)
|
|
134
|
+
.map(([key, value]) => `${key}=${compactText(value)}`)
|
|
135
|
+
.join(", ")}`])
|
|
136
|
+
])
|
|
137
|
+
];
|
|
138
|
+
return {
|
|
139
|
+
kind: "output",
|
|
140
|
+
output: `${lines.join("\n")}\n`,
|
|
141
|
+
data
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function renderResolvedInputRequest(request) {
|
|
145
|
+
if (request.status === "open")
|
|
146
|
+
return [];
|
|
147
|
+
return [
|
|
148
|
+
` ${request.id} [${request.status}]`,
|
|
149
|
+
` Question: ${compactText(request.question)}`,
|
|
150
|
+
...(request.status === "answered"
|
|
151
|
+
? [
|
|
152
|
+
` Answer: ${compactText(request.resolution.answer.text)}`,
|
|
153
|
+
` Answered by ${request.resolution.answeredBy} at ${request.resolution.answeredAt}`
|
|
154
|
+
]
|
|
155
|
+
: [
|
|
156
|
+
` Cancelled: ${compactText(request.cancellation.reason)}`,
|
|
157
|
+
` Cancelled at: ${request.cancellation.cancelledAt}`
|
|
158
|
+
])
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
function renderOpenInputRequest(request) {
|
|
162
|
+
const choices = request.choices.slice(0, RELATED_RECORD_LIMIT);
|
|
163
|
+
const blockedRefs = request.blockedRefs.slice(0, RELATED_RECORD_LIMIT);
|
|
164
|
+
const recommendedChoiceKey = request.policy.kind === "recommended"
|
|
165
|
+
? request.policy.recommendedChoiceKey
|
|
166
|
+
: undefined;
|
|
167
|
+
const recommendedChoice = recommendedChoiceKey === undefined
|
|
168
|
+
? undefined
|
|
169
|
+
: request.choices.find((choice) => choice.key === recommendedChoiceKey);
|
|
170
|
+
return [
|
|
171
|
+
` ${request.id} [${request.policy.kind}]`,
|
|
172
|
+
` Question: ${compactText(request.question)}`,
|
|
173
|
+
...(request.choices.length === 0
|
|
174
|
+
? [" Choices: none (free-text answer)."]
|
|
175
|
+
: [
|
|
176
|
+
` Choices (${choices.length}${request.choices.length > choices.length ? ` of ${request.choices.length}` : ""}):`,
|
|
177
|
+
...choices.map((choice) => ` ${choice.key}: ${compactText(choice.label)}`)
|
|
178
|
+
]),
|
|
179
|
+
...(request.policy.kind === "recommended"
|
|
180
|
+
? [
|
|
181
|
+
` Recommended choice: ${request.policy.recommendedChoiceKey}: ${compactText(recommendedChoice?.label ?? request.policy.recommendedChoiceKey)}`,
|
|
182
|
+
` Timeout at: ${request.policy.timeoutAt}`
|
|
183
|
+
]
|
|
184
|
+
: []),
|
|
185
|
+
...(request.blockedRefs.length === 0
|
|
186
|
+
? [" Blocks: none."]
|
|
187
|
+
: [
|
|
188
|
+
` Blocks (${blockedRefs.length}${request.blockedRefs.length > blockedRefs.length ? ` of ${request.blockedRefs.length}` : ""}):`,
|
|
189
|
+
...blockedRefs.map((reference) => ` ${reference.type}:${reference.id}`)
|
|
190
|
+
])
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
function recentSection(label, records, render) {
|
|
194
|
+
const recent = records.slice(-RECENT_RECORD_LIMIT);
|
|
195
|
+
const title = `Recent ${label} (${recent.length}${records.length > recent.length ? ` of ${records.length}` : ""}):`;
|
|
196
|
+
return recent.length === 0
|
|
197
|
+
? [title, " None."]
|
|
198
|
+
: [title, ...recent.flatMap(render)];
|
|
199
|
+
}
|
|
200
|
+
function currentAndRecentWorkItems(records) {
|
|
201
|
+
const current = records.filter((record) => !TERMINAL_WORK_ITEM_STATUSES.has(record.status));
|
|
202
|
+
if (current.length >= RECENT_RECORD_LIMIT)
|
|
203
|
+
return current;
|
|
204
|
+
const recentTerminal = records
|
|
205
|
+
.filter((record) => TERMINAL_WORK_ITEM_STATUSES.has(record.status))
|
|
206
|
+
.slice(-(RECENT_RECORD_LIMIT - current.length));
|
|
207
|
+
const selected = new Set([...current, ...recentTerminal]);
|
|
208
|
+
return records.filter((record) => selected.has(record));
|
|
209
|
+
}
|
|
210
|
+
function compactText(value) {
|
|
211
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
212
|
+
return oneLine.length <= SUMMARY_TEXT_LIMIT
|
|
213
|
+
? oneLine
|
|
214
|
+
: `${oneLine.slice(0, SUMMARY_TEXT_LIMIT - 3)}...`;
|
|
215
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { dataError, roleNotFound, taskNotFound, usageError } from "../errors/cliError.js";
|
|
2
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
|
|
4
|
+
import { answerInputRequest, cancelInputRequest, createInputRequest } from "../input/inputRequest.js";
|
|
5
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
|
+
import { updateRoleStatus } from "../role/role.js";
|
|
7
|
+
import { yieldAgentRun } from "../run/agentRun.js";
|
|
8
|
+
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
9
|
+
const LEADER_ROLE = "leader";
|
|
10
|
+
export function runTaskInputCommand(args, store, options) {
|
|
11
|
+
const [command, ...rest] = args;
|
|
12
|
+
switch (command) {
|
|
13
|
+
case "request": return createRequest(rest, store, options);
|
|
14
|
+
case "list": return listRequests(rest, store);
|
|
15
|
+
case "show": return showRequest(rest, store);
|
|
16
|
+
case "answer": return answerRequest(rest, store, options);
|
|
17
|
+
case "cancel": return cancelRequest(rest, store, options);
|
|
18
|
+
default:
|
|
19
|
+
throw usageError(command === undefined
|
|
20
|
+
? "Task input command is required."
|
|
21
|
+
: `Unknown command: task input ${command}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function openInputRequestCount(store, taskId) {
|
|
25
|
+
return store.listInputRequests(taskId).filter((request) => request.status === "open").length;
|
|
26
|
+
}
|
|
27
|
+
export function assertNoOpenInputRequests(store, taskId, action) {
|
|
28
|
+
const open = store.listInputRequests(taskId).find((request) => request.status === "open");
|
|
29
|
+
if (open !== undefined) {
|
|
30
|
+
throw usageError(`Task ${taskId} has open input ${open.id}; resolve it before ${action}.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function createRequest(args, store, options) {
|
|
34
|
+
const usage = "Task input request usage: yui task input request <task> --question <text> [--choice <key=label> ...] [--blocks <work-item:id|run:id> ...] [--recommend <key> --timeout-seconds <seconds>].";
|
|
35
|
+
const parsed = parseMultiValueTail(args, new Set(["--question", "--recommend", "--timeout-seconds"]), new Set(["--choice", "--blocks"]), usage);
|
|
36
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
37
|
+
const question = requiredOption(parsed.options, "--question");
|
|
38
|
+
const choices = (parsed.multiOptions.get("--choice") ?? []).map(parseInputChoice);
|
|
39
|
+
const blockedRefs = (parsed.multiOptions.get("--blocks") ?? []).map(parseInputBlockedRef);
|
|
40
|
+
const recommendedChoiceKey = optionalNonEmptyOption(parsed.options, "--recommend");
|
|
41
|
+
const timeoutSeconds = optionalNonEmptyOption(parsed.options, "--timeout-seconds");
|
|
42
|
+
if ((recommendedChoiceKey === undefined) !== (timeoutSeconds === undefined)) {
|
|
43
|
+
throw usageError("--recommend and --timeout-seconds must be used together.", usage);
|
|
44
|
+
}
|
|
45
|
+
const now = clock(options);
|
|
46
|
+
const policy = recommendedChoiceKey === undefined
|
|
47
|
+
? { kind: "required" }
|
|
48
|
+
: {
|
|
49
|
+
kind: "recommended",
|
|
50
|
+
recommendedChoiceKey,
|
|
51
|
+
timeoutAt: timeoutAfter(now, timeoutSeconds)
|
|
52
|
+
};
|
|
53
|
+
const request = store.transaction((tx) => {
|
|
54
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
55
|
+
if (task.status !== "active")
|
|
56
|
+
throw usageError(inactiveTaskMessage(task, "requesting input"));
|
|
57
|
+
validateBlockedInputOwnership(tx, task.id, blockedRefs);
|
|
58
|
+
const origin = requireLeaderInputOrigin(tx, task.id, options.environment);
|
|
59
|
+
const created = createInputRequest(tx.nextInputRequestId(task.id), task.id, origin.requester, { question, choices, blockedRefs, policy }, now);
|
|
60
|
+
tx.saveInputRequest(task.id, created);
|
|
61
|
+
tx.saveAgentRun(yieldAgentRun(origin.run, `Waiting for input ${created.id}: ${created.question}`, now));
|
|
62
|
+
tx.clearActiveAgentRun(task.id, LEADER_ROLE);
|
|
63
|
+
tx.saveRole(task.id, updateRoleStatus(origin.role, "idle", now));
|
|
64
|
+
if (origin.sessions?.sessions[origin.role.activeAgentId]?.status === "running") {
|
|
65
|
+
tx.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(origin.sessions, origin.role.activeAgentId, "ready", now));
|
|
66
|
+
}
|
|
67
|
+
recordTaskEvent(tx, task.id, "input.requested", {
|
|
68
|
+
requestId: created.id,
|
|
69
|
+
requesterRunId: created.requester.runId,
|
|
70
|
+
policy: created.policy.kind
|
|
71
|
+
}, now);
|
|
72
|
+
return created;
|
|
73
|
+
});
|
|
74
|
+
options.runtime?.notifyStateChanged(request.taskId);
|
|
75
|
+
return output(`Created input request ${request.id} for ${request.taskId}\n`, { request });
|
|
76
|
+
}
|
|
77
|
+
function listRequests(args, store) {
|
|
78
|
+
const usage = "Task input list usage: yui task input list [task] [--all].";
|
|
79
|
+
const parsed = parseTail(args, new Set(), usage, new Set(["--all"]));
|
|
80
|
+
if (parsed.positionals.length > 1)
|
|
81
|
+
throw usageError(usage);
|
|
82
|
+
const taskId = parsed.positionals[0];
|
|
83
|
+
if (taskId !== undefined)
|
|
84
|
+
requireTask(store, taskId);
|
|
85
|
+
const all = taskId === undefined
|
|
86
|
+
? store.listAllInputRequests()
|
|
87
|
+
: store.listInputRequests(taskId);
|
|
88
|
+
const requests = parsed.options.has("--all")
|
|
89
|
+
? all
|
|
90
|
+
: all.filter((request) => request.status === "open");
|
|
91
|
+
const rendered = requests.length === 0
|
|
92
|
+
? "No input requests found.\n"
|
|
93
|
+
: `${renderTable(taskId === undefined ? "Input inbox" : `Input requests: ${taskId}`, [
|
|
94
|
+
{ header: "Input", minWidth: 6, maxWidth: 18 },
|
|
95
|
+
{ header: "Task", minWidth: 6, maxWidth: 18 },
|
|
96
|
+
{ header: "Status", minWidth: 6, maxWidth: 10 },
|
|
97
|
+
{ header: "Policy", minWidth: 8, maxWidth: 12 },
|
|
98
|
+
{ header: "Question", minWidth: 8, maxWidth: 72 },
|
|
99
|
+
{ header: "Created", minWidth: 10, maxWidth: 28 }
|
|
100
|
+
], requests.map((request) => [
|
|
101
|
+
request.id,
|
|
102
|
+
request.taskId,
|
|
103
|
+
request.status,
|
|
104
|
+
request.policy.kind,
|
|
105
|
+
request.question,
|
|
106
|
+
request.createdAt
|
|
107
|
+
]), defaultTableWidth())}\n`;
|
|
108
|
+
return output(rendered, { requests });
|
|
109
|
+
}
|
|
110
|
+
function showRequest(args, store) {
|
|
111
|
+
const usage = "Task input show usage: yui task input show <input> [--task <task>].";
|
|
112
|
+
const parsed = parseTail(args, new Set(["--task"]), usage);
|
|
113
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
114
|
+
const taskId = optionalNonEmptyOption(parsed.options, "--task");
|
|
115
|
+
const request = taskId === undefined
|
|
116
|
+
? store.findInputRequest(parsed.positionals[0])
|
|
117
|
+
: store.getInputRequest(requireTask(store, taskId).id, parsed.positionals[0]);
|
|
118
|
+
if (request === null)
|
|
119
|
+
throw dataError(`Input request not found: ${parsed.positionals[0]}.`);
|
|
120
|
+
return output(renderInputRequest(request), { request });
|
|
121
|
+
}
|
|
122
|
+
function answerRequest(args, store, options) {
|
|
123
|
+
const usage = "Task input answer usage: yui task input answer <input> [--task <task>] (--choice <key> | --text <text>).";
|
|
124
|
+
const parsed = parseTail(args, new Set(["--task", "--choice", "--text"]), usage);
|
|
125
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
126
|
+
const choice = optionalNonEmptyOption(parsed.options, "--choice");
|
|
127
|
+
const text = optionalNonEmptyOption(parsed.options, "--text");
|
|
128
|
+
if ((choice === undefined) === (text === undefined)) {
|
|
129
|
+
throw usageError("Exactly one of --choice or --text is required.", usage);
|
|
130
|
+
}
|
|
131
|
+
const taskHint = optionalNonEmptyOption(parsed.options, "--task");
|
|
132
|
+
const located = taskHint === undefined
|
|
133
|
+
? store.findInputRequest(parsed.positionals[0])
|
|
134
|
+
: store.getInputRequest(requireTask(store, taskHint).id, parsed.positionals[0]);
|
|
135
|
+
if (located === null)
|
|
136
|
+
throw dataError(`Input request not found: ${parsed.positionals[0]}.`);
|
|
137
|
+
const answer = choice === undefined ? { text: text } : { choiceKey: choice };
|
|
138
|
+
const now = clock(options);
|
|
139
|
+
const request = store.transaction((tx) => {
|
|
140
|
+
const current = tx.getInputRequest(located.taskId, located.id);
|
|
141
|
+
if (current === null)
|
|
142
|
+
throw dataError(`Input request not found: ${located.id}.`);
|
|
143
|
+
const task = requireTask(tx, current.taskId);
|
|
144
|
+
if (task.status !== "active")
|
|
145
|
+
throw usageError(inactiveTaskMessage(task, "answering input"));
|
|
146
|
+
const answered = answerInputRequest(current, answer, inputAnswerer(options.environment), now);
|
|
147
|
+
tx.saveInputRequest(task.id, answered);
|
|
148
|
+
recordTaskEvent(tx, task.id, "input.answered", {
|
|
149
|
+
requestId: answered.id,
|
|
150
|
+
answeredBy: answered.resolution.answeredBy
|
|
151
|
+
}, now);
|
|
152
|
+
queueLeaderWakeup(tx, task.id, `input-answered:${answered.id}`, now);
|
|
153
|
+
return answered;
|
|
154
|
+
});
|
|
155
|
+
options.runtime?.notifyStateChanged(request.taskId);
|
|
156
|
+
return output(`Answered input request ${request.id} for ${request.taskId}\n`, { request });
|
|
157
|
+
}
|
|
158
|
+
function cancelRequest(args, store, options) {
|
|
159
|
+
const usage = "Task input cancel usage: yui task input cancel <task> <input> --reason <text>.";
|
|
160
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
161
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
162
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
163
|
+
const now = clock(options);
|
|
164
|
+
const request = store.transaction((tx) => {
|
|
165
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
166
|
+
if (task.status !== "active")
|
|
167
|
+
throw usageError(inactiveTaskMessage(task, "cancelling input"));
|
|
168
|
+
const current = tx.getInputRequest(task.id, parsed.positionals[1]);
|
|
169
|
+
if (current === null)
|
|
170
|
+
throw dataError(`Input request not found: ${parsed.positionals[1]}.`);
|
|
171
|
+
assertInputCancelOrigin(current, options.environment);
|
|
172
|
+
const cancelled = cancelInputRequest(current, reason, now);
|
|
173
|
+
tx.saveInputRequest(task.id, cancelled);
|
|
174
|
+
recordTaskEvent(tx, task.id, "input.cancelled", { requestId: cancelled.id }, now);
|
|
175
|
+
return cancelled;
|
|
176
|
+
});
|
|
177
|
+
options.runtime?.notifyStateChanged(request.taskId);
|
|
178
|
+
return output(`Cancelled input request ${request.id} for ${request.taskId}\n`, { request });
|
|
179
|
+
}
|
|
180
|
+
function requireLeaderInputOrigin(store, taskId, environment) {
|
|
181
|
+
const env = environment ?? {};
|
|
182
|
+
const role = requireRole(store, taskId, LEADER_ROLE);
|
|
183
|
+
const run = store.getActiveAgentRun(taskId, LEADER_ROLE);
|
|
184
|
+
if (env.YUI_SESSION_SCOPE !== "task"
|
|
185
|
+
|| env.YUI_TASK_ID !== taskId
|
|
186
|
+
|| env.YUI_ROLE !== LEADER_ROLE
|
|
187
|
+
|| env.YUI_AGENT_ID !== role.activeAgentId
|
|
188
|
+
|| run === null
|
|
189
|
+
|| env.YUI_RUN_ID !== run.id
|
|
190
|
+
|| run.status !== "active"
|
|
191
|
+
|| run.deliveredAt === undefined
|
|
192
|
+
|| run.workItemId !== undefined) {
|
|
193
|
+
throw usageError("Task input request requires the active Leader Run environment.");
|
|
194
|
+
}
|
|
195
|
+
const sessions = store.getTaskRoleSessionSet(taskId, LEADER_ROLE);
|
|
196
|
+
const nativeSessionId = trimmed(env.YUI_NATIVE_SESSION_ID);
|
|
197
|
+
if (nativeSessionId !== undefined
|
|
198
|
+
&& sessions?.sessions[role.activeAgentId]?.nativeSessionId !== nativeSessionId) {
|
|
199
|
+
throw usageError("Task input request native session does not match the active Leader session.");
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
requester: {
|
|
203
|
+
roleName: "leader",
|
|
204
|
+
agentId: role.activeAgentId,
|
|
205
|
+
runId: run.id,
|
|
206
|
+
...(nativeSessionId === undefined ? {} : { nativeSessionId })
|
|
207
|
+
},
|
|
208
|
+
role,
|
|
209
|
+
run,
|
|
210
|
+
sessions
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function assertInputCancelOrigin(request, environment) {
|
|
214
|
+
const env = environment ?? {};
|
|
215
|
+
if (env.YUI_SESSION_SCOPE !== "task"
|
|
216
|
+
|| env.YUI_TASK_ID !== request.taskId
|
|
217
|
+
|| env.YUI_ROLE !== request.requester.roleName
|
|
218
|
+
|| env.YUI_AGENT_ID !== request.requester.agentId
|
|
219
|
+
|| env.YUI_RUN_ID !== request.requester.runId
|
|
220
|
+
|| (request.requester.nativeSessionId !== undefined
|
|
221
|
+
&& env.YUI_NATIVE_SESSION_ID !== request.requester.nativeSessionId)) {
|
|
222
|
+
throw usageError("Only the originating Leader may cancel this input request.");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function inputAnswerer(environment) {
|
|
226
|
+
const env = environment ?? {};
|
|
227
|
+
if (env.YUI_SESSION_SCOPE === undefined && env.YUI_ROLE === undefined)
|
|
228
|
+
return "user";
|
|
229
|
+
if (env.YUI_SESSION_SCOPE === "global" && env.YUI_ROLE === "operator")
|
|
230
|
+
return "operator";
|
|
231
|
+
throw usageError("Task input answers may be submitted only by the user or Operator.");
|
|
232
|
+
}
|
|
233
|
+
function parseInputChoice(value) {
|
|
234
|
+
const separator = value.indexOf("=");
|
|
235
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
236
|
+
throw usageError("--choice must use key=label.");
|
|
237
|
+
}
|
|
238
|
+
return { key: value.slice(0, separator).trim(), label: value.slice(separator + 1).trim() };
|
|
239
|
+
}
|
|
240
|
+
function parseInputBlockedRef(value) {
|
|
241
|
+
const separator = value.indexOf(":");
|
|
242
|
+
const type = value.slice(0, separator);
|
|
243
|
+
const id = value.slice(separator + 1).trim();
|
|
244
|
+
if ((type !== "work-item" && type !== "run") || separator <= 0 || id.length === 0) {
|
|
245
|
+
throw usageError("--blocks must use work-item:<id> or run:<id>.");
|
|
246
|
+
}
|
|
247
|
+
return { type, id };
|
|
248
|
+
}
|
|
249
|
+
function validateBlockedInputOwnership(store, taskId, references) {
|
|
250
|
+
for (const reference of references) {
|
|
251
|
+
const record = reference.type === "work-item"
|
|
252
|
+
? store.findWorkItem(reference.id)
|
|
253
|
+
: store.findAgentRun(reference.id);
|
|
254
|
+
if (record === null)
|
|
255
|
+
throw dataError(`Blocked ${reference.type} not found: ${reference.id}.`);
|
|
256
|
+
if (record.taskId !== taskId) {
|
|
257
|
+
throw usageError(`Blocked ${reference.type} belongs to another Task: ${reference.id}.`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function renderInputRequest(request) {
|
|
262
|
+
return [
|
|
263
|
+
`Input: ${request.id}`,
|
|
264
|
+
`Task: ${request.taskId}`,
|
|
265
|
+
`Status: ${request.status}`,
|
|
266
|
+
`Question: ${request.question}`,
|
|
267
|
+
`Requested by: ${request.requester.agentId}/${request.requester.runId}`,
|
|
268
|
+
...(request.choices.length === 0
|
|
269
|
+
? ["Answer type: text"]
|
|
270
|
+
: ["Choices:", ...request.choices.map((choice) => ` ${choice.key}: ${choice.label}`)]),
|
|
271
|
+
...(request.blockedRefs.length === 0
|
|
272
|
+
? []
|
|
273
|
+
: ["Blocks:", ...request.blockedRefs.map((reference) => ` ${reference.type}:${reference.id}`)]),
|
|
274
|
+
...(request.policy.kind === "required"
|
|
275
|
+
? ["Policy: user response required"]
|
|
276
|
+
: [
|
|
277
|
+
`Policy: use recommended choice ${request.policy.recommendedChoiceKey} after timeout`,
|
|
278
|
+
`Timeout: ${request.policy.timeoutAt}`
|
|
279
|
+
]),
|
|
280
|
+
...(request.status === "answered"
|
|
281
|
+
? [
|
|
282
|
+
`Answered by: ${request.resolution.answeredBy}`,
|
|
283
|
+
`Answer: ${request.resolution.answer.text}`,
|
|
284
|
+
`Answered: ${request.resolution.answeredAt}`
|
|
285
|
+
]
|
|
286
|
+
: request.status === "cancelled"
|
|
287
|
+
? [
|
|
288
|
+
`Cancellation: ${request.cancellation.reason}`,
|
|
289
|
+
`Cancelled: ${request.cancellation.cancelledAt}`
|
|
290
|
+
]
|
|
291
|
+
: []),
|
|
292
|
+
`Created: ${request.createdAt}`,
|
|
293
|
+
`Updated: ${request.updatedAt}`
|
|
294
|
+
].join("\n").concat("\n");
|
|
295
|
+
}
|
|
296
|
+
function requireTask(store, taskId) {
|
|
297
|
+
const id = requiredText(taskId, "Task id");
|
|
298
|
+
const task = store.getTask(id);
|
|
299
|
+
if (task === null)
|
|
300
|
+
throw taskNotFound(id);
|
|
301
|
+
return task;
|
|
302
|
+
}
|
|
303
|
+
function requireRole(store, taskId, roleName) {
|
|
304
|
+
const role = store.getRole(taskId, roleName);
|
|
305
|
+
if (role === null)
|
|
306
|
+
throw roleNotFound(roleName);
|
|
307
|
+
return role;
|
|
308
|
+
}
|
|
309
|
+
function inactiveTaskMessage(task, action) {
|
|
310
|
+
if (task.status === "draft") {
|
|
311
|
+
return `Task ${task.id} is a Draft; activate it before ${action}.`;
|
|
312
|
+
}
|
|
313
|
+
if (task.status === "completed") {
|
|
314
|
+
return `Task ${task.id} is completed; reopen it before ${action}.`;
|
|
315
|
+
}
|
|
316
|
+
return `Task is archived: ${task.id}.`;
|
|
317
|
+
}
|
|
318
|
+
function requiredOption(options, name) {
|
|
319
|
+
return requiredText(options.get(name), name);
|
|
320
|
+
}
|
|
321
|
+
function optionalNonEmptyOption(options, name) {
|
|
322
|
+
if (!options.has(name))
|
|
323
|
+
return undefined;
|
|
324
|
+
return requiredText(options.get(name), name);
|
|
325
|
+
}
|
|
326
|
+
function output(value, data) {
|
|
327
|
+
return data === undefined
|
|
328
|
+
? { kind: "output", output: value }
|
|
329
|
+
: { kind: "output", output: value, data };
|
|
330
|
+
}
|
|
331
|
+
function clock(options) {
|
|
332
|
+
return options.now?.() ?? new Date();
|
|
333
|
+
}
|
|
334
|
+
function recordTaskEvent(store, taskId, type, payload, now) {
|
|
335
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), type, payload, now));
|
|
336
|
+
}
|
|
337
|
+
function requiredText(value, label) {
|
|
338
|
+
const normalized = value?.trim();
|
|
339
|
+
if (normalized === undefined || normalized.length === 0)
|
|
340
|
+
throw usageError(`${label} is required.`);
|
|
341
|
+
return normalized;
|
|
342
|
+
}
|
|
343
|
+
function trimmed(value) {
|
|
344
|
+
const normalized = value?.trim();
|
|
345
|
+
return normalized === undefined || normalized.length === 0 ? undefined : normalized;
|
|
346
|
+
}
|
|
347
|
+
function timeoutAfter(now, value) {
|
|
348
|
+
if (!/^[1-9][0-9]*$/.test(value)) {
|
|
349
|
+
throw usageError("--timeout-seconds must be a positive integer.");
|
|
350
|
+
}
|
|
351
|
+
const seconds = Number(value);
|
|
352
|
+
const timestamp = now.getTime() + seconds * 1_000;
|
|
353
|
+
const timeout = new Date(timestamp);
|
|
354
|
+
if (!Number.isSafeInteger(seconds) || !Number.isFinite(timeout.getTime())) {
|
|
355
|
+
throw usageError("--timeout-seconds is too large.");
|
|
356
|
+
}
|
|
357
|
+
return timeout.toISOString();
|
|
358
|
+
}
|
|
359
|
+
function exactPositionals(values, count, usage) {
|
|
360
|
+
if (values.length !== count || values.some((value) => value.trim().length === 0)) {
|
|
361
|
+
throw usageError(usage);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function parseTail(args, valueOptions, usage, flagOptions = new Set()) {
|
|
365
|
+
const positionals = [];
|
|
366
|
+
const options = new Map();
|
|
367
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
368
|
+
const value = args[index];
|
|
369
|
+
if (!value.startsWith("--")) {
|
|
370
|
+
positionals.push(value);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!valueOptions.has(value) && !flagOptions.has(value)) {
|
|
374
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
375
|
+
}
|
|
376
|
+
if (options.has(value))
|
|
377
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
378
|
+
if (flagOptions.has(value)) {
|
|
379
|
+
options.set(value, "");
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const optionValue = args[index + 1];
|
|
383
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
384
|
+
throw usageError(`${value} is required.`, usage);
|
|
385
|
+
}
|
|
386
|
+
options.set(value, optionValue);
|
|
387
|
+
index += 1;
|
|
388
|
+
}
|
|
389
|
+
return { positionals, options };
|
|
390
|
+
}
|
|
391
|
+
function parseMultiValueTail(args, valueOptions, repeatOptions, usage) {
|
|
392
|
+
const positionals = [];
|
|
393
|
+
const options = new Map();
|
|
394
|
+
const multiOptions = new Map();
|
|
395
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
396
|
+
const value = args[index];
|
|
397
|
+
if (!value.startsWith("--")) {
|
|
398
|
+
positionals.push(value);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!valueOptions.has(value) && !repeatOptions.has(value)) {
|
|
402
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
403
|
+
}
|
|
404
|
+
if (repeatOptions.has(value)) {
|
|
405
|
+
const optionValue = args[index + 1];
|
|
406
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
407
|
+
throw usageError(`${value} is required.`, usage);
|
|
408
|
+
}
|
|
409
|
+
multiOptions.set(value, [...(multiOptions.get(value) ?? []), optionValue]);
|
|
410
|
+
index += 1;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (options.has(value))
|
|
414
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
415
|
+
const optionValue = args[index + 1];
|
|
416
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
417
|
+
throw usageError(`${value} is required.`, usage);
|
|
418
|
+
}
|
|
419
|
+
options.set(value, optionValue);
|
|
420
|
+
index += 1;
|
|
421
|
+
}
|
|
422
|
+
return { positionals, options, multiOptions };
|
|
423
|
+
}
|