@hunsu/bridge 0.1.3 → 0.2.0-next.10

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.
@@ -1,72 +0,0 @@
1
- import type { CommitSha } from "@hunsu/core";
2
- import type { ExecutionPlan, PathId, PositiveInteger, Result, MemberPath } from "@hunsu/protocol";
3
-
4
- export type ExecuteOrchestrationErrorCode =
5
- | "invalid_start_state"
6
- | "active_execute_exists"
7
- | "team_output_invalid"
8
- | "execution_plan_invalid"
9
- | "goal_evaluation_invalid"
10
- | "path_dependency_unmet"
11
- | "terminal_output_missing"
12
- | "max_attempts_exhausted"
13
- | "side_effect_failed";
14
-
15
- export type ExecuteOrchestrationError = {
16
- code: ExecuteOrchestrationErrorCode;
17
- message: string;
18
- details?: unknown;
19
- };
20
-
21
- export type ExecuteStepResult<T> = Result<T, ExecuteOrchestrationError>;
22
-
23
- export type ExecutionPlanStepResult =
24
- | { type: "done"; terminalPath?: MemberPath; finalResponse?: string }
25
- | { type: "next"; execution: ExecutionPlan; terminalPath?: MemberPath; finalResponse?: string }
26
- | { type: "fail"; reason: ExecuteOrchestrationError; terminalPath?: MemberPath; finalResponse?: string };
27
-
28
- export type ExecuteRunStatus = "running" | "paused" | "arrived" | "accident" | "failed" | "discarded" | "finished" | "stopped";
29
-
30
- export type PathRunPlan = {
31
- path: MemberPath;
32
- dependencyPathIds: PathId[];
33
- dependencyOutputs: string[];
34
- isTerminal: boolean;
35
- };
36
-
37
- export type PathRunOutcome = {
38
- pathId: PathId;
39
- finalResponse: string;
40
- commit: CommitSha;
41
- parentCommit?: CommitSha;
42
- treeChanged: boolean;
43
- };
44
-
45
- export type ExecuteLoopDecision =
46
- | { type: "run_team_planning" }
47
- | { type: "run_member_path"; path: PathRunPlan }
48
- | { type: "record_accident"; reason: ExecuteOrchestrationError };
49
-
50
- export function executeError(
51
- code: ExecuteOrchestrationErrorCode,
52
- message: string,
53
- details?: unknown
54
- ): ExecuteOrchestrationError {
55
- return details === undefined ? { code, message } : { code, message, details };
56
- }
57
-
58
- export function executeErrorToError(error: ExecuteOrchestrationError): Error {
59
- const wrapped = new Error(error.message);
60
- wrapped.name = `ExecuteOrchestrationError:${error.code}`;
61
- if (error.details !== undefined) {
62
- (wrapped as Error & { details?: unknown }).details = error.details;
63
- }
64
- return wrapped;
65
- }
66
-
67
- export function unwrapExecuteResult<T>(result: ExecuteStepResult<T>): T {
68
- if (result.ok) {
69
- return result.value;
70
- }
71
- throw executeErrorToError(result.error);
72
- }
@@ -1,115 +0,0 @@
1
- import { err, makePositiveInteger, ok, type BoardProjection, type Destination, type LineRecord, type NodeRecord, type PositiveInteger } from "@hunsu/protocol";
2
- import {
3
- executeError,
4
- type ExecuteRunStatus,
5
- type ExecuteLoopDecision,
6
- type ExecuteStepResult
7
- } from "./execute-model.ts";
8
-
9
- export type ExecuteStartExistingRun = {
10
- sourceNodeId?: string;
11
- status: ExecuteRunStatus;
12
- };
13
-
14
- export type ExecuteStartPlanInput = {
15
- board: BoardProjection;
16
- line: LineRecord;
17
- lineNode: NodeRecord | undefined;
18
- existingRuns: readonly ExecuteStartExistingRun[];
19
- activeDestinations: readonly Destination[];
20
- selectedDestinationIds?: readonly string[];
21
- formatMovePosition?: (node: NodeRecord) => string;
22
- };
23
-
24
- export type ExecuteStartPlan = {
25
- lineNode: NodeRecord;
26
- selectedDestinationIds: string[];
27
- targetMoveOrdinal: PositiveInteger;
28
- };
29
-
30
- export function planExecuteStart(input: ExecuteStartPlanInput): ExecuteStepResult<ExecuteStartPlan> {
31
- const { line, lineNode } = input;
32
- if (!lineNode) {
33
- return err(executeError("invalid_start_state", `No execute-ready MOVE found for Team route ${line.id}`));
34
- }
35
- if (line.status === "failed" || line.status === "complete" || line.status === "abandoned") {
36
- return err(executeError("invalid_start_state", `Cannot start Execute from ${line.id} because the route is ${line.status}`));
37
- }
38
- const nodesWithNextMove = new Set(input.board.edges.filter(edge => edge.type === "move").map(edge => edge.fromNodeId));
39
- if (nodesWithNextMove.has(lineNode.id)) {
40
- return err(executeError(
41
- "invalid_start_state",
42
- `Cannot start Execute from ${formatMovePosition(lineNode, input.formatMovePosition)} because Arrived or Accident already exists`
43
- ));
44
- }
45
- const activeRun = input.existingRuns.find(existing =>
46
- existing.sourceNodeId === lineNode.id && (existing.status === "running" || existing.status === "paused")
47
- );
48
- if (activeRun) {
49
- return err(executeError(
50
- "active_execute_exists",
51
- `A Execute is already active for ${formatMovePosition(lineNode, input.formatMovePosition)}`
52
- ));
53
- }
54
- const targetMoveOrdinal = makePositiveInteger(lineNode.ordinal + 1, "targetMoveOrdinal");
55
- if (!targetMoveOrdinal.ok) {
56
- return err(executeError("invalid_start_state", targetMoveOrdinal.error.message));
57
- }
58
- const selectedDestinationIds = normalizeSelectedDestinationIds(input.selectedDestinationIds, input.activeDestinations);
59
- if (!selectedDestinationIds.ok) {
60
- return selectedDestinationIds;
61
- }
62
- return ok({
63
- lineNode,
64
- selectedDestinationIds: selectedDestinationIds.value,
65
- targetMoveOrdinal: targetMoveOrdinal.value
66
- });
67
- }
68
-
69
- export function ensureTerminalOutput(finalResponse: string | undefined): ExecuteStepResult<string> {
70
- if (!finalResponse) {
71
- return err(executeError("terminal_output_missing", "ExecutionPlan completed without a terminal output"));
72
- }
73
- return ok(finalResponse);
74
- }
75
-
76
- export function planMaxAttemptsExceeded(input: {
77
- maxAttemptCount: PositiveInteger;
78
- }): Extract<ExecuteLoopDecision, { type: "record_accident" }> {
79
- return {
80
- type: "record_accident",
81
- reason: executeError(
82
- "max_attempts_exhausted",
83
- `Execute exceeded max attempt count ${input.maxAttemptCount} without recording an Arrived MOVE`,
84
- { maxAttemptCount: input.maxAttemptCount }
85
- )
86
- };
87
- }
88
-
89
- function normalizeSelectedDestinationIds(
90
- selectedDestinationIds: readonly string[] | undefined,
91
- activeDestinations: readonly Destination[]
92
- ): ExecuteStepResult<string[]> {
93
- const nextDestination = nextQueuedDestination(activeDestinations);
94
- if (selectedDestinationIds && selectedDestinationIds.length > 0) {
95
- if (selectedDestinationIds.length !== 1) {
96
- return err(executeError("invalid_start_state", "Execute must select exactly one Destination"));
97
- }
98
- return nextDestination && selectedDestinationIds[0] === nextDestination.id
99
- ? ok([...selectedDestinationIds])
100
- : err(executeError("invalid_start_state", "Execute must select the next Destination in the queue"));
101
- }
102
- return ok(nextDestination ? [nextDestination.id] : []);
103
- }
104
-
105
- function nextQueuedDestination(activeDestinations: readonly Destination[]): Destination | undefined {
106
- return [...activeDestinations].sort(destinationQueueSort)[0];
107
- }
108
-
109
- function destinationQueueSort(left: Destination, right: Destination): number {
110
- return (right.priority ?? 0) - (left.priority ?? 0);
111
- }
112
-
113
- function formatMovePosition(node: NodeRecord, formatter: ((node: NodeRecord) => string) | undefined): string {
114
- return formatter ? formatter(node) : `${node.teamName ?? "Team"} MOVE ${node.ordinal}`;
115
- }
@@ -1,379 +0,0 @@
1
- import {
2
- err,
3
- makeNonEmptyText,
4
- makeNonNegativeInteger,
5
- ok,
6
- type GoalEvaluation,
7
- type GoalExecutionPlan,
8
- type ExecutionContinuation,
9
- type PathId,
10
- type ExecutionPlan,
11
- type ExecutorId,
12
- type MemberPath,
13
- type QueueExecutionPlan
14
- } from "@hunsu/protocol";
15
- import { executeError, type ExecuteStepResult } from "./execute-model.ts";
16
-
17
- export function parseExecutionPlan(finalResponse: string): ExecuteStepResult<ExecutionPlan> {
18
- const parsed = parseJsonObject(finalResponse, "Team planning final response must be a JSON ExecutionPlan object");
19
- if (!parsed.ok) {
20
- return parsed;
21
- }
22
- return decodeExecutionPlan(parsed.value, "execution");
23
- }
24
-
25
- export function parseGoalEvaluation(finalResponse: string): ExecuteStepResult<GoalEvaluation> {
26
- const parsed = parseJsonObject(finalResponse, "Goal evaluator final response must be a JSON object");
27
- if (!parsed.ok) {
28
- return parsed;
29
- }
30
- const record = parsed.value;
31
- if (record.type === "pass") {
32
- if (typeof record.summary !== "string" || record.summary.trim() === "") {
33
- return err(executeError("goal_evaluation_invalid", "pass evaluation must include a non-empty summary"));
34
- }
35
- const evidence = decodeOptionalStringArray(record.evidence, "evidence");
36
- if (!evidence.ok) {
37
- return evidence;
38
- }
39
- return ok({
40
- type: "pass",
41
- summary: record.summary,
42
- evidence: evidence.value
43
- });
44
- }
45
- if (record.type === "fail") {
46
- if (typeof record.reason !== "string" || record.reason.trim() === "") {
47
- return err(executeError("goal_evaluation_invalid", "fail evaluation must include a non-empty reason"));
48
- }
49
- if (typeof record.feedback !== "string" || record.feedback.trim() === "") {
50
- return err(executeError("goal_evaluation_invalid", "fail evaluation must include non-empty feedback"));
51
- }
52
- if (record.nextGoal !== undefined && typeof record.nextGoal !== "string") {
53
- return err(executeError("goal_evaluation_invalid", "fail evaluation nextGoal must be a string when present"));
54
- }
55
- const evidence = decodeOptionalStringArray(record.evidence, "evidence");
56
- if (!evidence.ok) {
57
- return evidence;
58
- }
59
- return ok({
60
- type: "fail",
61
- reason: record.reason,
62
- feedback: record.feedback,
63
- nextGoal: typeof record.nextGoal === "string" && record.nextGoal.trim() !== "" ? record.nextGoal : undefined,
64
- evidence: evidence.value
65
- });
66
- }
67
- return err(executeError("goal_evaluation_invalid", "Goal evaluator response type must be pass or fail"));
68
- }
69
-
70
- export function memberPathForGoalRole(
71
- execution: GoalExecutionPlan,
72
- role: "assignee" | "evaluator",
73
- pathId: string,
74
- requires: MemberPath["requires"]
75
- ): MemberPath {
76
- if (role === "assignee") {
77
- return {
78
- id: pathId,
79
- executorId: execution.assignee.executorId,
80
- goal: execution.assignee.goal,
81
- requires
82
- } as MemberPath;
83
- }
84
- const roleConfig = execution.evaluator;
85
- if (!roleConfig) {
86
- throw new Error(`Goal ${execution.id} does not define an evaluator`);
87
- }
88
- return {
89
- id: pathId,
90
- executorId: roleConfig.executorId,
91
- goal: roleConfig.prompt,
92
- requires
93
- } as MemberPath;
94
- }
95
-
96
- export function nextQueueExecution(queue: QueueExecutionPlan, headResult: ExecutionPlan | undefined): ExecutionPlan | undefined {
97
- const [, ...tail] = queue.items;
98
- if (headResult) {
99
- return { ...queue, items: [headResult, ...tail] };
100
- }
101
- if (tail.length === 0) {
102
- return undefined;
103
- }
104
- return { ...queue, items: tail };
105
- }
106
-
107
- function decodeExecutionPlan(value: unknown, path: string): ExecuteStepResult<ExecutionPlan> {
108
- if (!isRecord(value)) {
109
- return err(executeError("execution_plan_invalid", `${path} must be an object`));
110
- }
111
- if (value.kind === "queue") {
112
- return decodeQueueExecutionPlan(value, path);
113
- }
114
- if (value.kind === "goal") {
115
- return decodeGoalExecutionPlan(value, path);
116
- }
117
- if (value.kind === "continuation") {
118
- return decodeExecutionContinuation(value, path);
119
- }
120
- return err(executeError("execution_plan_invalid", `${path}.kind must be queue, goal, or continuation`));
121
- }
122
-
123
- function decodeQueueExecutionPlan(record: Record<string, unknown>, path: string): ExecuteStepResult<QueueExecutionPlan> {
124
- const id = decodeNonEmptyString(record.id, `${path}.id`);
125
- if (!id.ok) {
126
- return id;
127
- }
128
- if (!Array.isArray(record.items)) {
129
- return err(executeError("execution_plan_invalid", `${path}.items must be an array`));
130
- }
131
- const items: ExecutionPlan[] = [];
132
- for (const [index, item] of record.items.entries()) {
133
- const decoded = decodeExecutionPlan(item, `${path}.items[${index}]`);
134
- if (!decoded.ok) {
135
- return decoded;
136
- }
137
- items.push(decoded.value);
138
- }
139
- return ok({ kind: "queue", id: id.value, items } as QueueExecutionPlan);
140
- }
141
-
142
- function decodeGoalExecutionPlan(record: Record<string, unknown>, path: string): ExecuteStepResult<GoalExecutionPlan> {
143
- if (record.stage === "needs_evaluation") {
144
- return decodeGoalPathEvaluationStage(record, path);
145
- }
146
- if (record.stage === "needs_execution") {
147
- return decodeGoalExecutionPlanStage(record, path);
148
- }
149
- return err(executeError("execution_plan_invalid", `${path}.stage must be needs_evaluation or needs_execution`));
150
- }
151
-
152
- function decodeExecutionContinuation(record: Record<string, unknown>, path: string): ExecuteStepResult<ExecutionContinuation> {
153
- const id = decodePathId(record.id, `${path}.id`);
154
- if (!id.ok) {
155
- return id;
156
- }
157
- const teamScopeId = decodeMemberId(record.teamScopeId, `${path}.teamScopeId`);
158
- if (!teamScopeId.ok) {
159
- return teamScopeId;
160
- }
161
- const execution = decodeExecutionPlan(record.execution, `${path}.execution`);
162
- if (!execution.ok) {
163
- return execution;
164
- }
165
- const continuation = record.continuation === undefined
166
- ? ok(undefined)
167
- : isRecord(record.continuation)
168
- ? decodeGoalExecutionPlan(record.continuation, `${path}.continuation`)
169
- : err(executeError("execution_plan_invalid", `${path}.continuation must be an object`));
170
- if (!continuation.ok) {
171
- return continuation;
172
- }
173
- return ok({
174
- kind: "continuation",
175
- id: id.value,
176
- teamScopeId: teamScopeId.value,
177
- execution: execution.value,
178
- continuation: continuation.value
179
- });
180
- }
181
-
182
- function decodeGoalPathEvaluationStage(record: Record<string, unknown>, path: string): ExecuteStepResult<GoalExecutionPlan> {
183
- const id = decodePathId(record.id, `${path}.id`);
184
- if (!id.ok) {
185
- return id;
186
- }
187
- const assignee = decodeGoalRole(record.assignee, `${path}.assignee`, "goal");
188
- if (!assignee.ok) {
189
- return assignee;
190
- }
191
- const evaluator = record.evaluator === undefined || record.evaluator === null ? ok(undefined) : decodeGoalRole(record.evaluator, `${path}.evaluator`, "prompt");
192
- if (!evaluator.ok) {
193
- return evaluator;
194
- }
195
- const remainingAttempts = makeNonNegativeInteger(record.remainingAttempts, `${path}.remainingAttempts`);
196
- if (!remainingAttempts.ok) {
197
- return err(executeError("execution_plan_invalid", remainingAttempts.error.message));
198
- }
199
- const requires = decodePathRequires(record.requires, `${path}.requires`);
200
- if (!requires.ok) {
201
- return requires;
202
- }
203
- return ok({
204
- kind: "goal",
205
- stage: "needs_evaluation",
206
- id: id.value,
207
- assignee: {
208
- executorId: assignee.value.executorId,
209
- goal: assignee.value.text
210
- },
211
- evaluator: evaluator.value
212
- ? {
213
- executorId: evaluator.value.executorId,
214
- prompt: evaluator.value.text
215
- }
216
- : undefined,
217
- remainingAttempts: remainingAttempts.value,
218
- requires: requires.value
219
- } as GoalExecutionPlan);
220
- }
221
-
222
- function decodeGoalExecutionPlanStage(record: Record<string, unknown>, path: string): ExecuteStepResult<GoalExecutionPlan> {
223
- const decoded = decodeGoalPathEvaluationStage({ ...record, stage: "needs_evaluation" }, path);
224
- if (!decoded.ok) {
225
- return decoded;
226
- }
227
- const evaluationPathId = decodePathId(record.evaluationPathId, `${path}.evaluationPathId`);
228
- if (!evaluationPathId.ok) {
229
- return evaluationPathId;
230
- }
231
- const evaluation = decodeFailGoalEvaluation(record.evaluation, `${path}.evaluation`);
232
- if (!evaluation.ok) {
233
- return evaluation;
234
- }
235
- if (!Array.isArray(record.requires)) {
236
- return err(executeError("execution_plan_invalid", `${path}.requires must be an array for needs_execution`));
237
- }
238
- const requires = decodePathIdArray(record.requires, `${path}.requires`);
239
- if (!requires.ok) {
240
- return requires;
241
- }
242
- if (requires.value.length !== 1 || requires.value[0] !== evaluationPathId.value) {
243
- return err(executeError("execution_plan_invalid", `${path}.requires must contain only ${path}.evaluationPathId`));
244
- }
245
- return ok({
246
- ...decoded.value,
247
- stage: "needs_execution",
248
- evaluationPathId: evaluationPathId.value,
249
- evaluation: evaluation.value,
250
- requires: requires.value
251
- } as GoalExecutionPlan);
252
- }
253
-
254
- function decodeGoalRole(value: unknown, path: string, textField: "goal" | "prompt"): ExecuteStepResult<{ executorId: ExecutorId; text: string }> {
255
- if (!isRecord(value)) {
256
- return err(executeError("execution_plan_invalid", `${path} must be an object`));
257
- }
258
- const executorId = decodeMemberId(value.executorId, `${path}.executorId`);
259
- if (!executorId.ok) {
260
- return executorId;
261
- }
262
- const text = decodeNonEmptyString(value[textField], `${path}.${textField}`);
263
- if (!text.ok) {
264
- return text;
265
- }
266
- return ok({ executorId: executorId.value, text: text.value });
267
- }
268
-
269
- function decodeFailGoalEvaluation(value: unknown, path: string): ExecuteStepResult<Extract<GoalEvaluation, { type: "fail" }>> {
270
- if (!isRecord(value)) {
271
- return err(executeError("execution_plan_invalid", `${path} must be an object`));
272
- }
273
- if (value.type !== "fail") {
274
- return err(executeError("execution_plan_invalid", `${path}.type must be fail`));
275
- }
276
- if (typeof value.reason !== "string" || value.reason.trim() === "") {
277
- return err(executeError("execution_plan_invalid", `${path}.reason must be a non-empty string`));
278
- }
279
- if (typeof value.feedback !== "string" || value.feedback.trim() === "") {
280
- return err(executeError("execution_plan_invalid", `${path}.feedback must be a non-empty string`));
281
- }
282
- if (value.nextGoal !== undefined && typeof value.nextGoal !== "string") {
283
- return err(executeError("execution_plan_invalid", `${path}.nextGoal must be a string when present`));
284
- }
285
- const evidence = decodeOptionalStringArray(value.evidence, `${path}.evidence`);
286
- if (!evidence.ok) {
287
- return evidence;
288
- }
289
- return ok({
290
- type: "fail",
291
- reason: value.reason,
292
- feedback: value.feedback,
293
- nextGoal: typeof value.nextGoal === "string" && value.nextGoal.trim() !== "" ? value.nextGoal : undefined,
294
- evidence: evidence.value
295
- });
296
- }
297
-
298
- function decodeNonEmptyString(value: unknown, path: string): ExecuteStepResult<string> {
299
- if (typeof value !== "string" || value.trim() === "") {
300
- return err(executeError("execution_plan_invalid", `${path} must be a non-empty string`));
301
- }
302
- return ok(value);
303
- }
304
-
305
- function decodePathId(value: unknown, path: string): ExecuteStepResult<PathId> {
306
- const decoded = makeNonEmptyText(value, path);
307
- if (!decoded.ok) {
308
- return err(executeError("execution_plan_invalid", decoded.error.message));
309
- }
310
- return ok(decoded.value);
311
- }
312
-
313
- function decodeMemberId(value: unknown, path: string): ExecuteStepResult<ExecutorId> {
314
- const decoded = makeNonEmptyText(value, path);
315
- if (!decoded.ok) {
316
- return err(executeError("execution_plan_invalid", decoded.error.message));
317
- }
318
- return ok(decoded.value);
319
- }
320
-
321
- function decodePathRequires(value: unknown, path: string): ExecuteStepResult<MemberPath["requires"]> {
322
- if (value === "PrevMove") {
323
- return ok("PrevMove");
324
- }
325
- return decodePathIdArray(value, path);
326
- }
327
-
328
- function decodePathIdArray(value: unknown, path: string): ExecuteStepResult<PathId[]> {
329
- const decoded = decodeStringArray(value, path, "execution_plan_invalid");
330
- if (!decoded.ok) {
331
- return decoded;
332
- }
333
- const pathIds: PathId[] = [];
334
- for (const [index, item] of decoded.value.entries()) {
335
- const pathId = makeNonEmptyText(item, `${path}[${index}]`);
336
- if (!pathId.ok) {
337
- return err(executeError("execution_plan_invalid", pathId.error.message));
338
- }
339
- pathIds.push(pathId.value);
340
- }
341
- return ok(pathIds);
342
- }
343
-
344
- function decodeOptionalStringArray(value: unknown, path: string): ExecuteStepResult<string[] | undefined> {
345
- if (value === undefined) {
346
- return ok(undefined);
347
- }
348
- return decodeStringArray(value, path, "goal_evaluation_invalid");
349
- }
350
-
351
- function decodeStringArray(value: unknown, path: string, code: "goal_evaluation_invalid" | "execution_plan_invalid"): ExecuteStepResult<string[]> {
352
- if (!Array.isArray(value)) {
353
- return err(executeError(code, `${path} must be an array of strings`));
354
- }
355
- if (!value.every(item => typeof item === "string")) {
356
- return err(executeError(code, `${path} must be an array of strings`));
357
- }
358
- return ok([...value]);
359
- }
360
-
361
- function parseJsonObject(text: string, errorMessage: string): ExecuteStepResult<Record<string, unknown>> {
362
- try {
363
- const parsed = JSON.parse(text) as unknown;
364
- if (!isRecord(parsed)) {
365
- return err(executeError("execution_plan_invalid", errorMessage));
366
- }
367
- return ok(parsed);
368
- } catch (error) {
369
- return err(executeError(
370
- "execution_plan_invalid",
371
- errorMessage,
372
- { cause: error instanceof Error ? error.message : String(error) }
373
- ));
374
- }
375
- }
376
-
377
- function isRecord(value: unknown): value is Record<string, unknown> {
378
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
379
- }