@zq-silk/yui 0.15.8 → 0.15.9
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 +2 -0
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +211 -14
- package/dist/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/cli/commandCatalog.js +14 -10
- package/dist/cli.js +67 -0
- package/dist/commands/operatorCommands.js +33 -2
- package/dist/commands/taskActivationCommands.js +22 -0
- package/dist/commands/taskCommands.js +342 -79
- package/dist/context/runContextPack.js +28 -16
- package/dist/context/taskContext.js +26 -3
- package/dist/controller/controller.js +8 -2
- package/dist/kernel/builtinCapabilities.js +32 -24
- package/dist/message/message.js +56 -0
- package/dist/plugins/pluginService.js +11 -3
- package/dist/resources/projectResource.js +0 -48
- package/dist/resources/projectResourceService.js +3 -81
- package/dist/setup/setupCommand.js +3 -8
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/sqliteSchema.js +37 -3
- package/dist/storage/sqliteStore.js +1 -21
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -1
- package/dist/task/taskActivation.js +26 -0
- package/dist/task/taskActivationService.js +85 -69
- package/dist/task/taskSubmission.js +236 -0
- package/dist/web/assets/client/app.js +3 -2
- package/dist/web/assets/client/taskSurface.js +96 -7
- package/dist/web/webServer.js +18 -3
- package/dist/web/webTaskSurface.js +6 -6
- package/dist/workItem/workItem.js +14 -10
- package/docs/agent-result-consumption.md +2 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +2 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +44 -32
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -79
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +2 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +2 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +320 -217
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +2 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +2 -0
- package/docs/release-workflow.zh-CN.md +237 -0
- package/docs/roles-and-configuration.md +2 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +2 -0
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -57
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +2 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +2 -0
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +2 -0
- package/docs/testing/verification-levels.zh-CN.md +69 -0
- package/i18n/README.zh-CN.md +199 -10
- package/package.json +2 -1
- package/skills/yui-leader/SKILL.md +88 -331
- package/skills/yui-leader/references/execution.md +303 -0
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/task-plugins.md +8 -4
- package/skills/yui-operator/SKILL.md +16 -3
|
@@ -40,80 +40,96 @@ export function taskActivationOperationRef(taskId, requestId) {
|
|
|
40
40
|
* unchanged, so a retried tool call cannot produce a second activation.
|
|
41
41
|
*/
|
|
42
42
|
export function requestTaskActivation(store, input, now) {
|
|
43
|
-
return store.transaction((tx) =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (existing?.disposition === "pending") {
|
|
88
|
-
throw new Error(`Task already has a pending activation request: ${task.id}/${existing.operation.requestId}. `
|
|
89
|
-
+ "Cancel it before requesting different inputs.");
|
|
90
|
-
}
|
|
91
|
-
const request = createTaskActivationRequest(task.id, {
|
|
92
|
-
requestId: input.requestId,
|
|
93
|
-
actorId: input.actorId,
|
|
94
|
-
authorityRef: input.authorityRef,
|
|
43
|
+
return store.transaction((tx) => recordTaskActivationRequestInTransaction(tx, input, now));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The transactional core of {@link requestTaskActivation}, callable from inside
|
|
47
|
+
* a caller's own open transaction.
|
|
48
|
+
*
|
|
49
|
+
* The shared submission service (task-32 §2.3) records a develop route's
|
|
50
|
+
* activation request in the very transaction that saves the message, so the
|
|
51
|
+
* message and its activation intent commit together and no "submit then
|
|
52
|
+
* activate" two-command window exists. It reuses this exact logic rather than a
|
|
53
|
+
* second activation queue: the same idempotency, settled-id refusal and
|
|
54
|
+
* pending-collision rules apply whether the request arrives from the explicit
|
|
55
|
+
* boundary or from a develop submission. The caller owns transaction lifetime
|
|
56
|
+
* and any post-commit mailbox notification.
|
|
57
|
+
*/
|
|
58
|
+
export function recordTaskActivationRequestInTransaction(tx, input, now) {
|
|
59
|
+
const task = requireOpenDraft(tx, input.taskId);
|
|
60
|
+
const plan = canonicalEnvironmentPlan(input.environmentPlan);
|
|
61
|
+
const planningRun = resolveCallerPlanningRun(tx, task.id, input.callerRunId);
|
|
62
|
+
const startMode = planningRun === undefined
|
|
63
|
+
? "immediate"
|
|
64
|
+
: "after-planning-turn";
|
|
65
|
+
const existing = task.activationRequest;
|
|
66
|
+
// A requestId whose outcome was already decided is never re-opened, whether
|
|
67
|
+
// it still holds the slot or a later request displaced it. Cancelling A,
|
|
68
|
+
// requesting B and then retrying A must not resurrect A: the withdrawal is
|
|
69
|
+
// explicit authority, and adoption is exactly the effect it withdrew. An
|
|
70
|
+
// adopted id is equally final — its environment and status change happened.
|
|
71
|
+
// Only `failed` stays replayable, which is its documented contract.
|
|
72
|
+
//
|
|
73
|
+
// The authority is the durable activation event ledger, not the bounded
|
|
74
|
+
// display payload: the payload trims its oldest entries once it overflows,
|
|
75
|
+
// but the terminal events are never compacted, so an evicted cancellation is
|
|
76
|
+
// still refused with its original outcome instead of silently resurrecting.
|
|
77
|
+
const settled = settledActivationRequest(tx, task, input.requestId);
|
|
78
|
+
if (settled !== undefined) {
|
|
79
|
+
throw new Error(`Activation request ${input.requestId} was already ${settled.disposition} for ${task.id}`
|
|
80
|
+
+ `${settled.outcome === undefined ? "" : ` (${settled.outcome})`}. `
|
|
81
|
+
+ "Use a new requestId.");
|
|
82
|
+
}
|
|
83
|
+
if (existing?.operation.requestId === input.requestId) {
|
|
84
|
+
const digest = taskActivationInputDigest(task.id, {
|
|
95
85
|
startMode,
|
|
96
86
|
...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
|
|
97
87
|
environmentPlan: plan
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
startMode: request.startMode,
|
|
103
|
-
environmentPlan: describeEnvironmentPlan(request.environmentPlan),
|
|
104
|
-
actor: request.operation.actorId,
|
|
105
|
-
...(request.afterPlanningRun === undefined
|
|
106
|
-
? {}
|
|
107
|
-
: { afterPlanningRun: request.afterPlanningRun })
|
|
108
|
-
}, now));
|
|
88
|
+
});
|
|
89
|
+
if (existing.operation.inputDigest !== digest) {
|
|
90
|
+
throw new Error(`Activation requestId ${input.requestId} was already used for different inputs.`);
|
|
91
|
+
}
|
|
109
92
|
return {
|
|
110
|
-
operationRef: taskActivationOperationRef(task.id,
|
|
111
|
-
request,
|
|
112
|
-
startMode,
|
|
113
|
-
created:
|
|
114
|
-
...(
|
|
93
|
+
operationRef: taskActivationOperationRef(task.id, existing.operation.requestId),
|
|
94
|
+
request: existing,
|
|
95
|
+
startMode: existing.startMode,
|
|
96
|
+
created: false,
|
|
97
|
+
...(existing.afterPlanningRun === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { afterPlanningRun: existing.afterPlanningRun })
|
|
115
100
|
};
|
|
116
|
-
}
|
|
101
|
+
}
|
|
102
|
+
if (existing?.disposition === "pending") {
|
|
103
|
+
throw new Error(`Task already has a pending activation request: ${task.id}/${existing.operation.requestId}. `
|
|
104
|
+
+ "Cancel it before requesting different inputs.");
|
|
105
|
+
}
|
|
106
|
+
const request = createTaskActivationRequest(task.id, {
|
|
107
|
+
requestId: input.requestId,
|
|
108
|
+
actorId: input.actorId,
|
|
109
|
+
authorityRef: input.authorityRef,
|
|
110
|
+
startMode,
|
|
111
|
+
...(input.origin === undefined ? {} : { origin: input.origin }),
|
|
112
|
+
...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
|
|
113
|
+
environmentPlan: plan
|
|
114
|
+
}, now);
|
|
115
|
+
tx.saveTask(setTaskActivationRequest(task, request, now));
|
|
116
|
+
tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, TASK_ACTIVATION_EVENT.requested, {
|
|
117
|
+
requestId: request.operation.requestId,
|
|
118
|
+
startMode: request.startMode,
|
|
119
|
+
environmentPlan: describeEnvironmentPlan(request.environmentPlan),
|
|
120
|
+
actor: request.operation.actorId,
|
|
121
|
+
...(request.origin === undefined ? {} : { origin: request.origin }),
|
|
122
|
+
...(request.afterPlanningRun === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { afterPlanningRun: request.afterPlanningRun })
|
|
125
|
+
}, now));
|
|
126
|
+
return {
|
|
127
|
+
operationRef: taskActivationOperationRef(task.id, request.operation.requestId),
|
|
128
|
+
request,
|
|
129
|
+
startMode,
|
|
130
|
+
created: true,
|
|
131
|
+
...(planningRun === undefined ? {} : { afterPlanningRun: planningRun })
|
|
132
|
+
};
|
|
117
133
|
}
|
|
118
134
|
/** Explicit user or Operator cancellation. A cancelled request never adopts. */
|
|
119
135
|
export function cancelTaskActivation(store, taskId, requestId, reason, now) {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { SYSTEM_LEADER_ROLE } from "../role/systemRoles.js";
|
|
2
|
+
/**
|
|
3
|
+
* Requirement A phase and routing derivation (task-32 §2.1, §2.2).
|
|
4
|
+
*
|
|
5
|
+
* `Task.status` stays the only lifecycle statement. "Entered planning" is a
|
|
6
|
+
* derived fact, never a writable second state: this module reads it from the
|
|
7
|
+
* durable evidence a legitimate planning entry leaves behind, so idleness, a
|
|
8
|
+
* failed or replaced Session, a Controller restart, or a trimmed Context can
|
|
9
|
+
* never reset it to "not planned".
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The never-compacted event that records a Draft was routed into planning by an
|
|
13
|
+
* accepted user/operator submission, inside the same transaction that saved the
|
|
14
|
+
* message. It is one of the three independent derivation sources below, and the
|
|
15
|
+
* only one the shared submission service itself writes; the others are produced
|
|
16
|
+
* by the planning Run and its Session. Migration appends the same event for
|
|
17
|
+
* legitimate historical discussions so a post-upgrade develop cannot snatch a
|
|
18
|
+
* mid-discussion Draft into auto-activation.
|
|
19
|
+
*/
|
|
20
|
+
export const TASK_PLANNING_ENTERED_EVENT = "task.planning-entered";
|
|
21
|
+
/**
|
|
22
|
+
* Whether this Draft has already entered planning (task-32 §2.2).
|
|
23
|
+
*
|
|
24
|
+
* Derived — never a stored flag — from any of three independent, monotonic
|
|
25
|
+
* sources, each of which only ever comes into existence and never legitimately
|
|
26
|
+
* disappears:
|
|
27
|
+
*
|
|
28
|
+
* (a) a persisted planning-entered event, written in the message-save
|
|
29
|
+
* transaction the first time an accepted submission routed to planning;
|
|
30
|
+
* (b) a Leader planning AgentRun in history (the first-discuss Run, once the
|
|
31
|
+
* Controller has created it);
|
|
32
|
+
* (c) a Leader Session whose frozen launch carried planning authority.
|
|
33
|
+
*
|
|
34
|
+
* A role being configured, a non-empty body, or a non-empty Brief are
|
|
35
|
+
* deliberately NOT evidence: none of them proves the user's submission was ever
|
|
36
|
+
* accepted and routed. Only a Draft can be "in planning"; an active or terminal
|
|
37
|
+
* Task is past the question and answers false.
|
|
38
|
+
*/
|
|
39
|
+
export function draftHasEnteredPlanning(store, task) {
|
|
40
|
+
if (task.status !== "draft")
|
|
41
|
+
return false;
|
|
42
|
+
return hasPlanningEnteredEvent(store.listEvents(task.id))
|
|
43
|
+
|| hasLeaderPlanningRun(store, task.id)
|
|
44
|
+
|| hasLeaderPlanningSession(store, task.id);
|
|
45
|
+
}
|
|
46
|
+
function hasPlanningEnteredEvent(events) {
|
|
47
|
+
return events.some((event) => event.type === TASK_PLANNING_ENTERED_EVENT);
|
|
48
|
+
}
|
|
49
|
+
function hasLeaderPlanningRun(store, taskId) {
|
|
50
|
+
return store.listRuns(taskId).some((run) => run.roleName === SYSTEM_LEADER_ROLE && run.purpose === "planning");
|
|
51
|
+
}
|
|
52
|
+
function hasLeaderPlanningSession(store, taskId) {
|
|
53
|
+
const sessions = store.getTaskRoleSessionSet(taskId, SYSTEM_LEADER_ROLE);
|
|
54
|
+
if (sessions === null)
|
|
55
|
+
return false;
|
|
56
|
+
const candidates = [
|
|
57
|
+
...Object.values(sessions.sessions ?? {}),
|
|
58
|
+
...(sessions.history ?? [])
|
|
59
|
+
];
|
|
60
|
+
return candidates.some((session) => session.effective.executionAuthority === "planning");
|
|
61
|
+
}
|
|
62
|
+
export function draftActivationState(request) {
|
|
63
|
+
if (request === undefined)
|
|
64
|
+
return "none";
|
|
65
|
+
switch (request.disposition) {
|
|
66
|
+
case "pending": return "pending";
|
|
67
|
+
case "failed": return "failed";
|
|
68
|
+
case "adopted": return "adopted";
|
|
69
|
+
case "cancelled": return "none";
|
|
70
|
+
default: return "none";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decide how a user/operator submission routes, given its normalized intent and
|
|
75
|
+
* the phase/activation facts read inside the save transaction.
|
|
76
|
+
*
|
|
77
|
+
* This is a pure decision over already-read facts, kept separate from the
|
|
78
|
+
* transaction that saves the message and enqueues work so it can be unit-tested
|
|
79
|
+
* exhaustively against the §2.1 table without a Store. The winner of a race is
|
|
80
|
+
* decided by which submission's transaction commits first: the second one reads
|
|
81
|
+
* the phase/activation the first one already wrote and routes accordingly.
|
|
82
|
+
*
|
|
83
|
+
* `executionEnabled` is the Task's own gate, read in the same transaction. It
|
|
84
|
+
* only ever gates a *fresh* develop activation: recording an activation request
|
|
85
|
+
* against a stopped Draft is refused at the activation boundary, so routing that
|
|
86
|
+
* case to `activate` would abort the whole transaction and lose the saved
|
|
87
|
+
* message. Planning and already-open activation obligations are unaffected — the
|
|
88
|
+
* gate is enforced later at dispatch, exactly as it is for a plain message.
|
|
89
|
+
*/
|
|
90
|
+
export function decideSubmissionRouting(input) {
|
|
91
|
+
// record never wakes anything, in any state (§2.1 first column).
|
|
92
|
+
if (input.intent === "record")
|
|
93
|
+
return { kind: "record" };
|
|
94
|
+
if (input.status === "active") {
|
|
95
|
+
// Neither discuss nor develop re-activates or downgrades an active Task.
|
|
96
|
+
return { kind: "active-context" };
|
|
97
|
+
}
|
|
98
|
+
// Only Draft remains: terminal statuses are refused before routing (§2.1 last
|
|
99
|
+
// row) by the caller's assertTaskOpen, so this function is never asked about
|
|
100
|
+
// them.
|
|
101
|
+
if (input.activation === "pending") {
|
|
102
|
+
return input.intent === "develop"
|
|
103
|
+
// A compatible pending request already exists; reference it, never a
|
|
104
|
+
// second request (§2.3 "重复 develop 不产生第二个兼容激活请求").
|
|
105
|
+
? { kind: "await-activation", state: "pending" }
|
|
106
|
+
// discuss becomes post-activation input; it does not start new planning.
|
|
107
|
+
: { kind: "await-activation", state: "pending" };
|
|
108
|
+
}
|
|
109
|
+
if (input.activation === "failed") {
|
|
110
|
+
// develop never auto-retries or downgrades to planning; discuss reports the
|
|
111
|
+
// original failure and the explicit retry/cancel next step. Both save only.
|
|
112
|
+
return { kind: "await-activation", state: "failed" };
|
|
113
|
+
}
|
|
114
|
+
if (input.intent === "develop") {
|
|
115
|
+
// develop on an already-planned Draft never auto-activates — it needs
|
|
116
|
+
// explicit manual activation. On an unplanned Draft with no open activation
|
|
117
|
+
// it records the request and routes to activation, unless execution is
|
|
118
|
+
// stopped: then the message is saved but no activation is recorded.
|
|
119
|
+
if (input.enteredPlanning)
|
|
120
|
+
return { kind: "planned-needs-manual-activation" };
|
|
121
|
+
return input.executionEnabled
|
|
122
|
+
? { kind: "activate", environmentPlan: input.developEnvironmentPlan }
|
|
123
|
+
: { kind: "activation-blocked-execution-stopped" };
|
|
124
|
+
}
|
|
125
|
+
// discuss (the default): first discussion records the planning entry; later
|
|
126
|
+
// discussion continues the existing planning conversation.
|
|
127
|
+
return input.enteredPlanning
|
|
128
|
+
? { kind: "continue-planning" }
|
|
129
|
+
: { kind: "enter-planning" };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Normalize a possibly-absent submission intent to its effective value.
|
|
133
|
+
*
|
|
134
|
+
* An absent intent is `discuss` (task-32 §2.5: "默认/旧客户端都按 discuss"), so
|
|
135
|
+
* every old client and every message saved before the field existed keeps its
|
|
136
|
+
* historical Leader-waking behaviour. Body text is never inspected.
|
|
137
|
+
*
|
|
138
|
+
* A legacy `--wake-policy none` carried the "save only, do not wake" meaning that
|
|
139
|
+
* `record` now names, so a caller that supplied only that older signal maps to
|
|
140
|
+
* `record` rather than silently becoming a Leader-waking `discuss`. An explicit
|
|
141
|
+
* intent always wins over the wake-policy shorthand.
|
|
142
|
+
*/
|
|
143
|
+
export function normalizeSubmissionIntent(intent, wakePolicy) {
|
|
144
|
+
if (intent !== undefined)
|
|
145
|
+
return intent;
|
|
146
|
+
if (wakePolicy === "none")
|
|
147
|
+
return "record";
|
|
148
|
+
return "discuss";
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Whether a retry aims at the same target the key was first bound to (§2.3). An
|
|
152
|
+
* absent prior target (a keyed Message from before receipts existed) never
|
|
153
|
+
* matches, so such a Message is treated as a conflict rather than replayed from
|
|
154
|
+
* a fabricated disposition.
|
|
155
|
+
*/
|
|
156
|
+
export function sameSubmissionTarget(prior, next) {
|
|
157
|
+
if (prior === undefined || prior.kind !== next.kind)
|
|
158
|
+
return false;
|
|
159
|
+
if (prior.kind === "task" && next.kind === "task")
|
|
160
|
+
return prior.taskId === next.taskId;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Derive the §2.5 feedback for one committed submission from its routing and the
|
|
165
|
+
* facts read in the save transaction.
|
|
166
|
+
*
|
|
167
|
+
* Pure over already-read facts, so it is unit-testable against the whole table
|
|
168
|
+
* without a Store. `enteredPlanning` and `activationState` are the values read
|
|
169
|
+
* before routing executed; the routing itself names what the transaction then
|
|
170
|
+
* did, and phase is derived to reflect the post-commit truth (a just-entered
|
|
171
|
+
* planning Draft reports `draft-planning`).
|
|
172
|
+
*/
|
|
173
|
+
export function describeSubmissionFeedback(input) {
|
|
174
|
+
const { routing } = input;
|
|
175
|
+
const phase = input.status === "active"
|
|
176
|
+
? "active"
|
|
177
|
+
: routing.kind === "enter-planning"
|
|
178
|
+
|| routing.kind === "continue-planning"
|
|
179
|
+
|| routing.kind === "planned-needs-manual-activation"
|
|
180
|
+
|| input.enteredPlanning
|
|
181
|
+
? "draft-planning"
|
|
182
|
+
: "draft-unplanned";
|
|
183
|
+
const planning = routing.kind === "enter-planning"
|
|
184
|
+
? "entered"
|
|
185
|
+
: routing.kind === "continue-planning"
|
|
186
|
+
? "continued"
|
|
187
|
+
: "none";
|
|
188
|
+
const activation = routing.kind === "activate"
|
|
189
|
+
? "requested"
|
|
190
|
+
: routing.kind === "planned-needs-manual-activation"
|
|
191
|
+
? "manual-required"
|
|
192
|
+
: routing.kind === "activation-blocked-execution-stopped"
|
|
193
|
+
? "execution-stopped"
|
|
194
|
+
: routing.kind === "await-activation"
|
|
195
|
+
? routing.state
|
|
196
|
+
// record and the planning routes report the existing obligation, if
|
|
197
|
+
// any, truthfully without acting on it.
|
|
198
|
+
: input.activationState === "pending"
|
|
199
|
+
? "pending"
|
|
200
|
+
: input.activationState === "failed"
|
|
201
|
+
? "failed"
|
|
202
|
+
: "none";
|
|
203
|
+
const delivery = routing.kind === "enter-planning"
|
|
204
|
+
|| routing.kind === "continue-planning"
|
|
205
|
+
|| routing.kind === "active-context"
|
|
206
|
+
? "queued"
|
|
207
|
+
: "none";
|
|
208
|
+
const nextStep = submissionNextStep(input);
|
|
209
|
+
return {
|
|
210
|
+
saved: { taskId: input.taskId, messageId: input.messageId },
|
|
211
|
+
phase,
|
|
212
|
+
planning,
|
|
213
|
+
activation,
|
|
214
|
+
delivery,
|
|
215
|
+
...(nextStep === undefined ? {} : { nextStep })
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function submissionNextStep(input) {
|
|
219
|
+
if (input.routing.kind === "planned-needs-manual-activation") {
|
|
220
|
+
return { kind: "activate-manually", taskId: input.taskId };
|
|
221
|
+
}
|
|
222
|
+
if (input.routing.kind === "activation-blocked-execution-stopped") {
|
|
223
|
+
return { kind: "start-execution", taskId: input.taskId };
|
|
224
|
+
}
|
|
225
|
+
if (input.routing.kind === "await-activation") {
|
|
226
|
+
if (input.routing.state === "failed") {
|
|
227
|
+
return {
|
|
228
|
+
kind: "resolve-failed-activation",
|
|
229
|
+
activationRef: input.activationRef ?? "",
|
|
230
|
+
failure: input.activationFailure ?? "activation failed"
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { kind: "await-pending-activation", activationRef: input.activationRef ?? "" };
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
@@ -128,8 +128,9 @@ function detailActions() {
|
|
|
128
128
|
answerInput: answerInput,
|
|
129
129
|
openTerminal: openTerminal,
|
|
130
130
|
inspect: inspectRecord,
|
|
131
|
-
sendMessage: (taskId, body, requestId) => submitMutation(taskId + "/messages",
|
|
132
|
-
"/api/tasks/" + encodeURIComponent(taskId) + "/messages",
|
|
131
|
+
sendMessage: (taskId, body, requestId, intent) => submitMutation(taskId + "/messages",
|
|
132
|
+
"/api/tasks/" + encodeURIComponent(taskId) + "/messages",
|
|
133
|
+
{ body, requestId, ...(intent === undefined ? {} : { intent }) }),
|
|
133
134
|
updateTask: (taskId, patch, requestId) => submitMutation(taskId + "/metadata",
|
|
134
135
|
"/api/tasks/" + encodeURIComponent(taskId) + "/metadata", { patch, requestId }),
|
|
135
136
|
panels: (taskId) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels",
|
|
@@ -33,27 +33,47 @@ export function renderTaskSurface(container, data, t, locale, actions) {
|
|
|
33
33
|
message.required = true;
|
|
34
34
|
message.maxLength = 8000;
|
|
35
35
|
chatLabel.append(message);
|
|
36
|
+
// Submission intent is an explicit user choice, never inferred from the body
|
|
37
|
+
// (task-32 §2.5); discuss is the default so the control matches the service.
|
|
38
|
+
const intentLabel = node("label", "", say("Intent", "提交意图"));
|
|
39
|
+
const intentSelect = node("select", "");
|
|
40
|
+
const intentOptions = [
|
|
41
|
+
["discuss", say("Discuss — route to planning", "讨论 — 进入规划")],
|
|
42
|
+
["record", say("Record — save only", "记录 — 仅保存")],
|
|
43
|
+
["develop", say("Develop — request activation", "开发 — 请求激活")]
|
|
44
|
+
];
|
|
45
|
+
for (const [value, text] of intentOptions) {
|
|
46
|
+
const option = node("option", "", text);
|
|
47
|
+
option.value = value;
|
|
48
|
+
intentSelect.append(option);
|
|
49
|
+
}
|
|
50
|
+
intentSelect.value = "discuss";
|
|
51
|
+
intentLabel.append(intentSelect);
|
|
36
52
|
chat.addEventListener("input", () => { chat.dataset.unsent = message.value ? "true" : "false"; });
|
|
37
53
|
const send = node("button", "record-open", say("Send", "发送"));
|
|
38
54
|
send.type = "submit";
|
|
39
55
|
send.disabled = !["active", "draft"].includes(task.status);
|
|
40
56
|
const sent = node("p", "muted", say("Not submitted", "未提交"));
|
|
41
57
|
sent.setAttribute("role", "status");
|
|
42
|
-
|
|
58
|
+
// §2.5 facets render here, one per line; cleared and repopulated on every submit.
|
|
59
|
+
const facets = node("div", "section-body");
|
|
60
|
+
facets.dataset.submissionFacets = "";
|
|
61
|
+
chat.append(chatLabel, intentLabel, send, sent, facets);
|
|
43
62
|
chat.addEventListener("submit", async (event) => {
|
|
44
63
|
event.preventDefault();
|
|
45
64
|
if (send.disabled) return;
|
|
46
65
|
send.disabled = true;
|
|
47
66
|
chat.dataset.unsent = "true";
|
|
48
67
|
const requestId = crypto.randomUUID();
|
|
68
|
+
const intent = intentSelect.value;
|
|
69
|
+
clear(facets);
|
|
49
70
|
sent.textContent = say("Waiting for receipt · ", "等待回执 · ") + requestId;
|
|
50
71
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
+ receipt.record.id;
|
|
72
|
+
// requestId is the submission key: the same key retried returns the original
|
|
73
|
+
// Message and routing rather than a second submission (task-32 §2.3).
|
|
74
|
+
const receipt = await actions.sendMessage(task.id, message.value, requestId, intent);
|
|
75
|
+
sent.textContent = say("Saved · ", "已保存 · ") + receipt.record.id;
|
|
76
|
+
renderSubmissionFacets(facets, receipt.submission, say);
|
|
57
77
|
message.value = "";
|
|
58
78
|
chat.dataset.unsent = "false";
|
|
59
79
|
send.disabled = false;
|
|
@@ -63,6 +83,9 @@ export function renderTaskSurface(container, data, t, locale, actions) {
|
|
|
63
83
|
send.disabled = false;
|
|
64
84
|
return;
|
|
65
85
|
}
|
|
86
|
+
// A lost response must never auto-replay a write; the same requestId is safe
|
|
87
|
+
// to resend by hand because the server dedups on it (§2.3), but the user
|
|
88
|
+
// reloads and inspects first rather than blindly resending.
|
|
66
89
|
sent.textContent = say("Unknown submission outcome. Reload and inspect saved messages before sending again · ",
|
|
67
90
|
"提交结果未知。请重新加载并检查已保存消息,不要盲目重发 · ") + requestId;
|
|
68
91
|
}
|
|
@@ -350,4 +373,70 @@ export function renderTaskSurface(container, data, t, locale, actions) {
|
|
|
350
373
|
scaffold.append(panels);
|
|
351
374
|
container.append(scaffold);
|
|
352
375
|
}
|
|
376
|
+
|
|
377
|
+
// Render each task-32 §2.5 submission facet on its own line, never collapsed into
|
|
378
|
+
// a single "started". This repo has one current server contract: every submission
|
|
379
|
+
// returns its faceted receipt, so there is no old-server flat-disposition path to
|
|
380
|
+
// fall back to. A missing facet block is a contract violation, reported as a
|
|
381
|
+
// limited diagnostic rather than reinterpreted as a plausible flat result.
|
|
382
|
+
function renderSubmissionFacets(container, submission, say) {
|
|
383
|
+
clear(container);
|
|
384
|
+
if (!submission) {
|
|
385
|
+
container.append(node("p", "muted",
|
|
386
|
+
say("Submission receipt is missing its §2.5 facets.", "提交回执缺少 §2.5 分面字段。")));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const phaseText = {
|
|
390
|
+
"active": say("Active — delivery context", "进行中 — 交付上下文"),
|
|
391
|
+
"draft-planning": say("Draft, in planning", "草稿,规划中"),
|
|
392
|
+
"draft-unplanned": say("Draft, not yet planned", "草稿,尚未规划")
|
|
393
|
+
};
|
|
394
|
+
const planningText = {
|
|
395
|
+
"entered": say("Entered planning", "已进入规划"),
|
|
396
|
+
"continued": say("Continued planning", "继续规划"),
|
|
397
|
+
"none": say("No planning change", "规划无变化")
|
|
398
|
+
};
|
|
399
|
+
const activationText = {
|
|
400
|
+
"requested": say("Activation requested", "已请求激活"),
|
|
401
|
+
"pending": say("Activation pending", "激活待处理"),
|
|
402
|
+
"failed": say("Activation failed", "激活失败"),
|
|
403
|
+
"manual-required": say("Manual activation required", "需手动激活"),
|
|
404
|
+
"execution-stopped": say("Execution stopped; not activated", "执行已停止;未激活"),
|
|
405
|
+
"none": say("No activation", "无激活")
|
|
406
|
+
};
|
|
407
|
+
const deliveryText = {
|
|
408
|
+
"queued": say("Leader queued to act now", "已为 Leader 排队处理"),
|
|
409
|
+
"none": say("Leader not woken", "未唤醒 Leader")
|
|
410
|
+
};
|
|
411
|
+
const facts = [
|
|
412
|
+
[say("Phase", "阶段"), phaseText[submission.phase] ?? submission.phase],
|
|
413
|
+
[say("Planning", "规划"), planningText[submission.planning] ?? submission.planning],
|
|
414
|
+
[say("Activation", "激活"), activationText[submission.activation] ?? submission.activation],
|
|
415
|
+
[say("Delivery", "投递"), deliveryText[submission.delivery] ?? submission.delivery]
|
|
416
|
+
];
|
|
417
|
+
for (const [label, value] of facts) {
|
|
418
|
+
container.append(node("p", "muted", label + ": " + value));
|
|
419
|
+
}
|
|
420
|
+
const step = submission.nextStep;
|
|
421
|
+
if (step) {
|
|
422
|
+
const nextText = step.kind === "activate-manually"
|
|
423
|
+
? say("Next: activate this Task explicitly (yui task activate " + step.taskId + ").",
|
|
424
|
+
"下一步:显式激活该 Task(yui task activate " + step.taskId + ")。")
|
|
425
|
+
: step.kind === "start-execution"
|
|
426
|
+
? say("Next: start execution first, then activate (" + step.taskId + ").",
|
|
427
|
+
"下一步:先启动执行,再激活(" + step.taskId + ")。")
|
|
428
|
+
: step.kind === "await-pending-activation"
|
|
429
|
+
? say("Next: waiting on the pending activation " + step.activationRef + ".",
|
|
430
|
+
"下一步:等待待处理的激活 " + step.activationRef + "。")
|
|
431
|
+
: step.kind === "resolve-failed-activation"
|
|
432
|
+
? say("Next: the prior activation failed (" + step.failure + "); retry with a new request or cancel " + step.activationRef + ".",
|
|
433
|
+
"下一步:之前的激活失败(" + step.failure + ");请用新请求重试或取消 " + step.activationRef + "。")
|
|
434
|
+
: "";
|
|
435
|
+
if (nextText) {
|
|
436
|
+
const next = node("p", "", nextText);
|
|
437
|
+
next.dataset.nextStep = step.kind;
|
|
438
|
+
container.append(next);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
353
442
|
`;
|
package/dist/web/webServer.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createServer } from "node:http";
|
|
|
3
3
|
import WebSocket, { WebSocketServer } from "ws";
|
|
4
4
|
import { usageError } from "../errors/cliError.js";
|
|
5
5
|
import { WebRequestRejected } from "./webMutation.js";
|
|
6
|
+
import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
|
|
6
7
|
import { DASHBOARD_HTML, findWebAsset } from "./assets/assetManifest.js";
|
|
7
8
|
import { buildWebDashboardSnapshot, buildWebTaskDetail } from "./webSnapshot.js";
|
|
8
9
|
const MAX_JSON_BODY_BYTES = 16 * 1024;
|
|
@@ -160,11 +161,13 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
160
161
|
else if (method === "POST" && action === "messages") {
|
|
161
162
|
const body = await readMutationBody(request);
|
|
162
163
|
if (typeof body !== "object" || body === null || Array.isArray(body)
|
|
163
|
-
|| Object.keys(body).some((key) => !["body", "requestId"].includes(key))
|
|
164
|
+
|| Object.keys(body).some((key) => !["body", "requestId", "intent"].includes(key))
|
|
164
165
|
|| !("requestId" in body) || typeof body.requestId !== "string" || !body.requestId.trim()
|
|
165
166
|
|| !("body" in body) || typeof body.body !== "string")
|
|
166
|
-
throw new WebRequestRejected("Expected body and
|
|
167
|
-
|
|
167
|
+
throw new WebRequestRejected("Expected body, requestId and optional intent.");
|
|
168
|
+
const intent = webSubmissionIntent(body);
|
|
169
|
+
// requestId is threaded as the submission key (§2.3) and echoed back on the receipt.
|
|
170
|
+
value = { ...dependencies.surface.message(taskId, body.body, intent, body.requestId), requestId: body.requestId };
|
|
168
171
|
}
|
|
169
172
|
else if (method === "POST" && action === "metadata") {
|
|
170
173
|
const body = await readMutationBody(request);
|
|
@@ -450,6 +453,18 @@ async function readMutationBody(request) {
|
|
|
450
453
|
throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid request body.");
|
|
451
454
|
}
|
|
452
455
|
}
|
|
456
|
+
/** Validate an optional submission intent from a Web message body. Absent leaves
|
|
457
|
+
* it undefined so the shared service applies the discuss default (task-32 §2.5).
|
|
458
|
+
* A present-but-invalid value is rejected rather than silently downgraded. */
|
|
459
|
+
function webSubmissionIntent(body) {
|
|
460
|
+
if (!("intent" in body) || body.intent === undefined)
|
|
461
|
+
return undefined;
|
|
462
|
+
if (typeof body.intent === "string"
|
|
463
|
+
&& TASK_SUBMISSION_INTENTS.includes(body.intent)) {
|
|
464
|
+
return body.intent;
|
|
465
|
+
}
|
|
466
|
+
throw new WebRequestRejected(`intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}.`);
|
|
467
|
+
}
|
|
453
468
|
async function readJsonBody(request) {
|
|
454
469
|
const chunks = [];
|
|
455
470
|
let size = 0;
|
|
@@ -22,16 +22,16 @@ export function createWebTaskSurface(store, options = {}, observations = []) {
|
|
|
22
22
|
options.runtime?.notifyStateChanged(taskId);
|
|
23
23
|
};
|
|
24
24
|
return {
|
|
25
|
-
message: (taskId, body) => {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
const { message, task, queuedForLeader } = webLocalMutation(store, (tx) => sendTaskMessageCommand(tx, taskId, body, "leader", commandOptions));
|
|
25
|
+
message: (taskId, body, intent, requestId) => {
|
|
26
|
+
// With no explicit intent the Web surface submits `discuss` like every other
|
|
27
|
+
// client (§2.5), through the one shared service; the requestId is threaded as
|
|
28
|
+
// the submission key (§2.3) and the structured feedback is returned verbatim.
|
|
29
|
+
const { message, task, queuedForLeader, feedback } = webLocalMutation(store, (tx) => sendTaskMessageCommand(tx, taskId, body, undefined, commandOptions, undefined, intent, requestId));
|
|
31
30
|
notify(taskId, queuedForLeader);
|
|
32
31
|
return { record: message, revision: message.createdAt,
|
|
33
32
|
disposition: queuedForLeader ? "queued" : "saved",
|
|
34
33
|
planning: task.status === "draft",
|
|
34
|
+
...(feedback === undefined ? {} : { submission: feedback }),
|
|
35
35
|
target: { scope: "task", taskId, roleName: "leader" } };
|
|
36
36
|
},
|
|
37
37
|
read: async (taskId) => withContextObservations(readTaskContext(store, taskId, environment), observations),
|