@axiom-lattice/protocols 3.0.3 → 4.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +46 -0
- package/dist/index.d.mts +219 -151
- package/dist/index.d.ts +219 -151
- package/dist/index.js +226 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +222 -12
- package/dist/index.mjs.map +1 -1
- package/jest.config.js +19 -0
- package/package.json +5 -1
- package/src/A2AApiKeyStoreProtocol.ts +13 -5
- package/src/A2AProtocol.ts +34 -174
- package/src/EvalStoreProtocol.ts +2 -1
- package/src/SkillStoreProtocol.ts +21 -0
- package/src/TaskBeliefProtocol.ts +324 -0
- package/src/TaskStoreProtocol.ts +94 -3
- package/src/TaskWorkItemProtocol.ts +32 -0
- package/src/WorkspaceStoreProtocol.ts +21 -1
- package/src/__tests__/TaskBeliefProtocol.test.ts +421 -0
- package/src/__tests__/a2a-types.test.ts +30 -0
- package/src/index.ts +1 -0
package/src/TaskStoreProtocol.ts
CHANGED
|
@@ -122,6 +122,12 @@ export interface TaskItem {
|
|
|
122
122
|
* Create task request type
|
|
123
123
|
*/
|
|
124
124
|
export interface CreateTaskRequest {
|
|
125
|
+
/**
|
|
126
|
+
* Caller-provided task identifier. When set, the store uses this id instead of
|
|
127
|
+
* generating one — used for single-ID mapping with external systems (e.g. A2A).
|
|
128
|
+
*/
|
|
129
|
+
id?: string;
|
|
130
|
+
|
|
125
131
|
/**
|
|
126
132
|
* Task title
|
|
127
133
|
*/
|
|
@@ -226,6 +232,8 @@ export interface TaskFileRef {
|
|
|
226
232
|
uri: string;
|
|
227
233
|
/** Display name (useful when uri is a uuid or bare path) */
|
|
228
234
|
name?: string;
|
|
235
|
+
/** MIME type of the referenced file (e.g. "application/pdf"), when known */
|
|
236
|
+
mimeType?: string;
|
|
229
237
|
/** Who attached the file: the user (reference material) or an agent (artifact) */
|
|
230
238
|
addedBy?: "user" | "agent";
|
|
231
239
|
}
|
|
@@ -287,7 +295,7 @@ export interface UpdateTaskRequest {
|
|
|
287
295
|
/**
|
|
288
296
|
* Additional contextual data
|
|
289
297
|
*/
|
|
290
|
-
context?: Record<string, unknown
|
|
298
|
+
context?: Record<string, unknown> | null;
|
|
291
299
|
|
|
292
300
|
/**
|
|
293
301
|
* Owner type
|
|
@@ -312,12 +320,12 @@ export interface UpdateTaskRequest {
|
|
|
312
320
|
/**
|
|
313
321
|
* Task result output
|
|
314
322
|
*/
|
|
315
|
-
result?: string;
|
|
323
|
+
result?: string | null;
|
|
316
324
|
|
|
317
325
|
/**
|
|
318
326
|
* Reason for task failure (when status is 'failed')
|
|
319
327
|
*/
|
|
320
|
-
failureReason?: string;
|
|
328
|
+
failureReason?: string | null;
|
|
321
329
|
|
|
322
330
|
/** File references attached to this task */
|
|
323
331
|
files?: TaskFileRef[];
|
|
@@ -424,6 +432,89 @@ export interface TaskStore {
|
|
|
424
432
|
*/
|
|
425
433
|
update(tenantId: string, id: string, updates: UpdateTaskRequest): Promise<TaskItem | null>;
|
|
426
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Atomically update a task only when its current status is expected.
|
|
437
|
+
*
|
|
438
|
+
* Storage implementations must evaluate the status predicate in the same
|
|
439
|
+
* atomic mutation that applies `updates`; callers must not emulate this with
|
|
440
|
+
* a separate read followed by {@link update}.
|
|
441
|
+
*
|
|
442
|
+
* @param tenantId Tenant identifier.
|
|
443
|
+
* @param id Task identifier.
|
|
444
|
+
* @param updates Partial task data to update.
|
|
445
|
+
* @param expectedStatuses Current statuses that permit the update.
|
|
446
|
+
* @returns The updated task, or `null` when the task is missing or its status is not expected.
|
|
447
|
+
*/
|
|
448
|
+
updateIfStatusIn(
|
|
449
|
+
tenantId: string,
|
|
450
|
+
id: string,
|
|
451
|
+
updates: UpdateTaskRequest,
|
|
452
|
+
expectedStatuses: TaskItem["status"][],
|
|
453
|
+
): Promise<TaskItem | null>;
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Atomically updates a task only when its status and update timestamp match a read snapshot.
|
|
457
|
+
*
|
|
458
|
+
* Implementations must normalize `Date` and string timestamps to the same stable ISO
|
|
459
|
+
* representation and evaluate both predicates in the mutation itself.
|
|
460
|
+
*
|
|
461
|
+
* @param tenantId Tenant identifier.
|
|
462
|
+
* @param id Task identifier.
|
|
463
|
+
* @param updates Partial task data to update.
|
|
464
|
+
* @param expectedStatuses Current statuses that permit the update.
|
|
465
|
+
* @param expectedUpdatedAt Update timestamp captured from the validated task snapshot.
|
|
466
|
+
* @returns The updated task, or `null` when the task is missing or either snapshot predicate differs.
|
|
467
|
+
*/
|
|
468
|
+
updateIfStatusAndUpdatedAt(
|
|
469
|
+
tenantId: string,
|
|
470
|
+
id: string,
|
|
471
|
+
updates: UpdateTaskRequest,
|
|
472
|
+
expectedStatuses: TaskItem["status"][],
|
|
473
|
+
expectedUpdatedAt: Date | string,
|
|
474
|
+
): Promise<TaskItem | null>;
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Atomically updates a child only when both child and parent snapshots match.
|
|
478
|
+
*
|
|
479
|
+
* @param tenantId Tenant identifier shared by the child and parent.
|
|
480
|
+
* @param id Child task identifier.
|
|
481
|
+
* @param updates Partial child task data to update.
|
|
482
|
+
* @param expectedStatuses Child statuses that permit the update.
|
|
483
|
+
* @param expectedUpdatedAt Child update timestamp captured during validation.
|
|
484
|
+
* @param parentId Parent task identifier captured during validation.
|
|
485
|
+
* @param expectedParentUpdatedAt Parent update timestamp captured during validation.
|
|
486
|
+
* @returns The updated child, or `null` when either task is missing or either snapshot differs.
|
|
487
|
+
*/
|
|
488
|
+
updateIfStatusUpdatedAtAndParentUpdatedAt(
|
|
489
|
+
tenantId: string,
|
|
490
|
+
id: string,
|
|
491
|
+
updates: UpdateTaskRequest,
|
|
492
|
+
expectedStatuses: TaskItem["status"][],
|
|
493
|
+
expectedUpdatedAt: Date | string,
|
|
494
|
+
parentId: string,
|
|
495
|
+
expectedParentUpdatedAt: Date | string,
|
|
496
|
+
): Promise<TaskItem | null>;
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Atomically update a task unless its current status is blocked.
|
|
500
|
+
*
|
|
501
|
+
* Storage implementations must evaluate the status predicate in the same
|
|
502
|
+
* atomic mutation that applies `updates`; callers must not emulate this with
|
|
503
|
+
* a separate read followed by {@link update}.
|
|
504
|
+
*
|
|
505
|
+
* @param tenantId Tenant identifier.
|
|
506
|
+
* @param id Task identifier.
|
|
507
|
+
* @param updates Partial task data to update.
|
|
508
|
+
* @param blockedStatuses Current statuses that prevent the update.
|
|
509
|
+
* @returns The updated task, or `null` when the task is missing or blocked.
|
|
510
|
+
*/
|
|
511
|
+
updateIfStatusNotIn(
|
|
512
|
+
tenantId: string,
|
|
513
|
+
id: string,
|
|
514
|
+
updates: UpdateTaskRequest,
|
|
515
|
+
blockedStatuses: TaskItem["status"][],
|
|
516
|
+
): Promise<TaskItem | null>;
|
|
517
|
+
|
|
427
518
|
/**
|
|
428
519
|
* Delete a task by ID
|
|
429
520
|
* @param tenantId Tenant identifier
|
|
@@ -17,6 +17,8 @@ export interface TaskWorkItem {
|
|
|
17
17
|
summary?: string;
|
|
18
18
|
detail?: Record<string, unknown>;
|
|
19
19
|
attempt?: number;
|
|
20
|
+
/** Deterministic task-scoped identity used for idempotent event replay. */
|
|
21
|
+
eventKey?: string;
|
|
20
22
|
createdAt: Date;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -33,12 +35,23 @@ export interface CreateWorkItemRequest {
|
|
|
33
35
|
attempt?: number;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Work-item creation request requiring a deterministic event identity.
|
|
40
|
+
*
|
|
41
|
+
* Event keys are unique within a tenant and task, not globally.
|
|
42
|
+
*/
|
|
43
|
+
export interface CreateWorkItemIfAbsentRequest extends CreateWorkItemRequest {
|
|
44
|
+
/** Deterministic task-scoped identity used for idempotent event replay. */
|
|
45
|
+
eventKey: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export interface TaskWorkItemListFilter {
|
|
37
49
|
tenantId: string;
|
|
38
50
|
taskId: string;
|
|
39
51
|
workspaceId?: string;
|
|
40
52
|
projectId?: string;
|
|
41
53
|
action?: string;
|
|
54
|
+
order?: 'asc' | 'desc';
|
|
42
55
|
limit?: number;
|
|
43
56
|
offset?: number;
|
|
44
57
|
}
|
|
@@ -46,4 +59,23 @@ export interface TaskWorkItemListFilter {
|
|
|
46
59
|
export interface TaskWorkItemStore {
|
|
47
60
|
create(params: CreateWorkItemRequest): Promise<TaskWorkItem>;
|
|
48
61
|
list(filter: TaskWorkItemListFilter): Promise<TaskWorkItem[]>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Find an event by deterministic identity without list pagination.
|
|
65
|
+
*
|
|
66
|
+
* @param tenantId Tenant identifier.
|
|
67
|
+
* @param taskId Task identifier that scopes the event key.
|
|
68
|
+
* @param eventKey Deterministic event identity.
|
|
69
|
+
* @returns The matching item, or `null` when absent.
|
|
70
|
+
*/
|
|
71
|
+
findByEventKey(tenantId: string, taskId: string, eventKey: string): Promise<TaskWorkItem | null>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Atomically create an event unless its task-scoped key already exists.
|
|
75
|
+
* Existing events are returned unchanged, preserving immutable replay.
|
|
76
|
+
*
|
|
77
|
+
* @param params Work-item fields including the required event key.
|
|
78
|
+
* @returns The existing or newly created work item.
|
|
79
|
+
*/
|
|
80
|
+
createIfAbsentByEventKey(params: CreateWorkItemIfAbsentRequest): Promise<TaskWorkItem>;
|
|
49
81
|
}
|
|
@@ -50,6 +50,13 @@ export interface WorkspaceStore {
|
|
|
50
50
|
deleteWorkspace(tenantId: string, id: string): Promise<boolean>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Project kind classification
|
|
55
|
+
*
|
|
56
|
+
* Defaults to "business" for legacy rows and omitted input.
|
|
57
|
+
*/
|
|
58
|
+
export type ProjectKind = "business" | "training" | "personal";
|
|
59
|
+
|
|
53
60
|
/**
|
|
54
61
|
* Project type definition
|
|
55
62
|
*/
|
|
@@ -61,6 +68,8 @@ export interface Project {
|
|
|
61
68
|
description?: string;
|
|
62
69
|
/** Application-specific configuration stored as JSON */
|
|
63
70
|
config?: Record<string, unknown>;
|
|
71
|
+
/** Project classification; defaults to "business" when omitted */
|
|
72
|
+
kind?: ProjectKind;
|
|
64
73
|
createdAt: Date;
|
|
65
74
|
updatedAt: Date;
|
|
66
75
|
}
|
|
@@ -73,6 +82,8 @@ export interface CreateProjectRequest {
|
|
|
73
82
|
description?: string;
|
|
74
83
|
/** Application-specific configuration stored as JSON (optional) */
|
|
75
84
|
config?: Record<string, unknown>;
|
|
85
|
+
/** Project classification; defaults to "business" when omitted */
|
|
86
|
+
kind?: ProjectKind;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
/**
|
|
@@ -87,6 +98,15 @@ export interface UpdateProjectRequest {
|
|
|
87
98
|
description?: string;
|
|
88
99
|
/** Application-specific configuration stored as JSON (replaces existing if provided) */
|
|
89
100
|
config?: Record<string, unknown>;
|
|
101
|
+
/** Project classification */
|
|
102
|
+
kind?: ProjectKind;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Filter options for listing projects within a workspace
|
|
107
|
+
*/
|
|
108
|
+
export interface ProjectFilter {
|
|
109
|
+
kind?: ProjectKind;
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
/**
|
|
@@ -94,7 +114,7 @@ export interface UpdateProjectRequest {
|
|
|
94
114
|
* Provides CRUD operations for project data
|
|
95
115
|
*/
|
|
96
116
|
export interface ProjectStore {
|
|
97
|
-
getProjectsByWorkspace(tenantId: string, workspaceId: string): Promise<Project[]>;
|
|
117
|
+
getProjectsByWorkspace(tenantId: string, workspaceId: string, filter?: ProjectFilter): Promise<Project[]>;
|
|
98
118
|
getProjectById(tenantId: string, id: string): Promise<Project | null>;
|
|
99
119
|
createProject(tenantId: string, workspaceId: string, id: string, data: CreateProjectRequest): Promise<Project>;
|
|
100
120
|
updateProject(tenantId: string, id: string, updates: UpdateProjectRequest): Promise<Project | null>;
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseTaskBeliefState,
|
|
3
|
+
replaceTaskBeliefState,
|
|
4
|
+
taskBeliefStatesEqual,
|
|
5
|
+
type TaskBeliefState,
|
|
6
|
+
} from "../TaskBeliefProtocol";
|
|
7
|
+
|
|
8
|
+
const HEADER = "| Belief Key | Probability | Target | Basis |";
|
|
9
|
+
const SEPARATOR = "|---|---:|---:|---|";
|
|
10
|
+
|
|
11
|
+
function beliefSection(rows: string[]): string {
|
|
12
|
+
return ["## Belief State", "", HEADER, SEPARATOR, ...rows].join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("parseTaskBeliefState", () => {
|
|
16
|
+
it("parses the canonical four-column Belief State", () => {
|
|
17
|
+
const markdown = [
|
|
18
|
+
"## Objective",
|
|
19
|
+
"Ship",
|
|
20
|
+
"",
|
|
21
|
+
beliefSection(["| `input-valid` | 60% | 90% | File exists |"]),
|
|
22
|
+
].join("\n");
|
|
23
|
+
|
|
24
|
+
expect(parseTaskBeliefState(markdown)).toEqual({
|
|
25
|
+
success: true,
|
|
26
|
+
state: {
|
|
27
|
+
entries: [
|
|
28
|
+
{ key: "input-valid", probability: 60, target: 90, basis: "File exists" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("accepts escaped pipes in Basis", () => {
|
|
35
|
+
const result = parseTaskBeliefState(
|
|
36
|
+
beliefSection(["| `input-valid` | 60% | 90% | JSON parser A \\| B passed |"]),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual({
|
|
40
|
+
success: true,
|
|
41
|
+
state: {
|
|
42
|
+
entries: [
|
|
43
|
+
{
|
|
44
|
+
key: "input-valid",
|
|
45
|
+
probability: 60,
|
|
46
|
+
target: 90,
|
|
47
|
+
basis: "JSON parser A | B passed",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("stops the section at an ATX heading with up to three leading spaces", () => {
|
|
55
|
+
const markdown = [
|
|
56
|
+
beliefSection(["| `input-valid` | 60% | 90% | File exists |"]),
|
|
57
|
+
"",
|
|
58
|
+
" ## Notes",
|
|
59
|
+
"This is not a belief row.",
|
|
60
|
+
].join("\n");
|
|
61
|
+
|
|
62
|
+
expect(parseTaskBeliefState(markdown)).toEqual({
|
|
63
|
+
success: true,
|
|
64
|
+
state: {
|
|
65
|
+
entries: [
|
|
66
|
+
{ key: "input-valid", probability: 60, target: 90, basis: "File exists" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects a row whose apparent closing pipe is escaped", () => {
|
|
73
|
+
const result = parseTaskBeliefState(
|
|
74
|
+
beliefSection(["| `input-valid` | 60% | 90% | basis \\|"]),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(result).toMatchObject({
|
|
78
|
+
success: false,
|
|
79
|
+
code: "MALFORMED_BELIEF_ROW",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("parses a Basis ending in a literal pipe followed by a closing delimiter", () => {
|
|
84
|
+
const result = parseTaskBeliefState(
|
|
85
|
+
beliefSection(["| `input-valid` | 60% | 90% | basis \\| |"]),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
success: true,
|
|
90
|
+
state: {
|
|
91
|
+
entries: [
|
|
92
|
+
{ key: "input-valid", probability: 60, target: 90, basis: "basis |" },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("ignores a fake section inside a code fence", () => {
|
|
99
|
+
const markdown = [
|
|
100
|
+
"```markdown",
|
|
101
|
+
beliefSection(["| `fake-key` | 1% | 2% | fake |"]),
|
|
102
|
+
"```",
|
|
103
|
+
"",
|
|
104
|
+
beliefSection(["| `real-key` | 40% | 80% | observed |"]),
|
|
105
|
+
].join("\n");
|
|
106
|
+
|
|
107
|
+
expect(parseTaskBeliefState(markdown)).toMatchObject({
|
|
108
|
+
success: true,
|
|
109
|
+
state: { entries: [{ key: "real-key" }] },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it.each([
|
|
114
|
+
["backtick", "```markdown", "```not-a-close", "```"],
|
|
115
|
+
["tilde", "~~~markdown", "~~~not-a-close", "~~~"],
|
|
116
|
+
])("keeps headings fenced after a malformed %s close candidate", (_, opener, malformedClose, close) => {
|
|
117
|
+
const markdown = [
|
|
118
|
+
opener,
|
|
119
|
+
malformedClose,
|
|
120
|
+
beliefSection(["| `fake-key` | 1% | 2% | fake |"]),
|
|
121
|
+
close,
|
|
122
|
+
].join("\n");
|
|
123
|
+
|
|
124
|
+
expect(parseTaskBeliefState(markdown)).toMatchObject({
|
|
125
|
+
success: false,
|
|
126
|
+
code: "MISSING_BELIEF_STATE",
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("reports a missing non-fenced section", () => {
|
|
131
|
+
const markdown = [
|
|
132
|
+
"## Objective",
|
|
133
|
+
"Ship",
|
|
134
|
+
"",
|
|
135
|
+
"~~~markdown",
|
|
136
|
+
beliefSection(["| `fake-key` | 1% | 2% | fake |"]),
|
|
137
|
+
"~~~",
|
|
138
|
+
].join("\n");
|
|
139
|
+
|
|
140
|
+
expect(parseTaskBeliefState(markdown)).toMatchObject({
|
|
141
|
+
success: false,
|
|
142
|
+
code: "MISSING_BELIEF_STATE",
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("reports duplicate non-fenced sections", () => {
|
|
147
|
+
const markdown = [
|
|
148
|
+
beliefSection(["| `first-key` | 10% | 90% | first |"]),
|
|
149
|
+
"",
|
|
150
|
+
beliefSection(["| `second-key` | 20% | 90% | second |"]),
|
|
151
|
+
].join("\n");
|
|
152
|
+
|
|
153
|
+
expect(parseTaskBeliefState(markdown)).toMatchObject({
|
|
154
|
+
success: false,
|
|
155
|
+
code: "DUPLICATE_BELIEF_STATE",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("reports duplicate keys", () => {
|
|
160
|
+
const result = parseTaskBeliefState(
|
|
161
|
+
beliefSection([
|
|
162
|
+
"| `same-key` | 10% | 90% | one |",
|
|
163
|
+
"| `same-key` | 20% | 90% | two |",
|
|
164
|
+
]),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(result).toMatchObject({
|
|
168
|
+
success: false,
|
|
169
|
+
code: "DUPLICATE_BELIEF_KEY",
|
|
170
|
+
key: "same-key",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("reports localized or reordered headers", () => {
|
|
175
|
+
const markdown = [
|
|
176
|
+
"## Belief State",
|
|
177
|
+
"",
|
|
178
|
+
"| Belief Key | Target | Probability | Basis |",
|
|
179
|
+
SEPARATOR,
|
|
180
|
+
"| `input-valid` | 90% | 60% | exists |",
|
|
181
|
+
].join("\n");
|
|
182
|
+
|
|
183
|
+
expect(parseTaskBeliefState(markdown)).toMatchObject({
|
|
184
|
+
success: false,
|
|
185
|
+
code: "INVALID_BELIEF_HEADERS",
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it.each(["60.5%", "-1%", "101%", "60", " 60 % "])(
|
|
190
|
+
"reports malformed probability %s",
|
|
191
|
+
(probability) => {
|
|
192
|
+
const result = parseTaskBeliefState(
|
|
193
|
+
beliefSection([`| \`input-valid\` | ${probability} | 90% | exists |`]),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(result).toMatchObject({
|
|
197
|
+
success: false,
|
|
198
|
+
code: "INVALID_BELIEF_PERCENT",
|
|
199
|
+
column: "probability",
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
it("reports malformed target percentages", () => {
|
|
205
|
+
const result = parseTaskBeliefState(
|
|
206
|
+
beliefSection(["| `input-valid` | 60% | nope | exists |"]),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(result).toMatchObject({
|
|
210
|
+
success: false,
|
|
211
|
+
code: "INVALID_BELIEF_PERCENT",
|
|
212
|
+
column: "target",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it.each(["input-valid", "`Input-valid`", "`input_valid`", "`-input-valid`"]) (
|
|
217
|
+
"reports malformed belief key %s",
|
|
218
|
+
(key) => {
|
|
219
|
+
const result = parseTaskBeliefState(
|
|
220
|
+
beliefSection([`| ${key} | 60% | 90% | exists |`]),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(result).toMatchObject({
|
|
224
|
+
success: false,
|
|
225
|
+
code: "INVALID_BELIEF_KEY",
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
it("reports malformed rows", () => {
|
|
231
|
+
const result = parseTaskBeliefState(
|
|
232
|
+
beliefSection(["| `input-valid` | 60% | 90% | basis | extra |"]),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(result).toMatchObject({
|
|
236
|
+
success: false,
|
|
237
|
+
code: "MALFORMED_BELIEF_ROW",
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("taskBeliefStatesEqual", () => {
|
|
243
|
+
it("ignores row order and insignificant whitespace", () => {
|
|
244
|
+
const left: TaskBeliefState = {
|
|
245
|
+
entries: [
|
|
246
|
+
{ key: "alpha-ready", probability: 20, target: 80, basis: " first observation " },
|
|
247
|
+
{ key: "beta-ready", probability: 30, target: 90, basis: "line one\n line two" },
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
const right: TaskBeliefState = {
|
|
251
|
+
entries: [
|
|
252
|
+
{ key: "beta-ready", probability: 30, target: 90, basis: "line one line two" },
|
|
253
|
+
{ key: "alpha-ready", probability: 20, target: 80, basis: "first observation" },
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
expect(taskBeliefStatesEqual(left, right)).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it.each(["probability", "target", "basis"] as const)("compares %s", (field) => {
|
|
261
|
+
const left: TaskBeliefState = {
|
|
262
|
+
entries: [{ key: "input-valid", probability: 60, target: 90, basis: "exists" }],
|
|
263
|
+
};
|
|
264
|
+
const changed = {
|
|
265
|
+
probability: 61,
|
|
266
|
+
target: 91,
|
|
267
|
+
basis: "verified",
|
|
268
|
+
}[field];
|
|
269
|
+
const right: TaskBeliefState = {
|
|
270
|
+
entries: [{ ...left.entries[0], [field]: changed }],
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
expect(taskBeliefStatesEqual(left, right)).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("replaceTaskBeliefState", () => {
|
|
278
|
+
const replacement: TaskBeliefState = {
|
|
279
|
+
entries: [
|
|
280
|
+
{ key: "input-valid", probability: 75, target: 90, basis: "A | B verified" },
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
it.each([
|
|
285
|
+
["literal pipe", "|", "\\|"],
|
|
286
|
+
["backslash before pipe", "\\|", "\\\\\\|"],
|
|
287
|
+
["trailing backslash and pipe", "evidence \\|", "evidence \\\\\\|"],
|
|
288
|
+
["multiple backslashes before pipe", "\\\\|", "\\\\\\\\\\|"],
|
|
289
|
+
["plain backslash", "path\\segment", "path\\\\segment"],
|
|
290
|
+
])("round-trips a Basis containing %s", (_, basis, escapedBasis) => {
|
|
291
|
+
const state: TaskBeliefState = {
|
|
292
|
+
entries: [{ key: "input-valid", probability: 75, target: 90, basis }],
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const replaced = replaceTaskBeliefState("# Task", state);
|
|
296
|
+
expect(replaced).toContain(`| \`input-valid\` | 75% | 90% | ${escapedBasis} |`);
|
|
297
|
+
|
|
298
|
+
const parsed = parseTaskBeliefState(replaced);
|
|
299
|
+
expect(parsed.success).toBe(true);
|
|
300
|
+
if (parsed.success) {
|
|
301
|
+
expect(taskBeliefStatesEqual(parsed.state, state)).toBe(true);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("replaces only the existing Belief State section", () => {
|
|
306
|
+
const before = [
|
|
307
|
+
"# Task",
|
|
308
|
+
"",
|
|
309
|
+
"## Objective",
|
|
310
|
+
"Keep this text exactly. ",
|
|
311
|
+
"",
|
|
312
|
+
beliefSection(["| `old-key` | 10% | 80% | old |"]),
|
|
313
|
+
"",
|
|
314
|
+
"## Acceptance Criteria",
|
|
315
|
+
"- [ ] Preserve this | text",
|
|
316
|
+
"",
|
|
317
|
+
].join("\n");
|
|
318
|
+
|
|
319
|
+
const replaced = replaceTaskBeliefState(before, replacement);
|
|
320
|
+
|
|
321
|
+
expect(replaced).toBe([
|
|
322
|
+
"# Task",
|
|
323
|
+
"",
|
|
324
|
+
"## Objective",
|
|
325
|
+
"Keep this text exactly. ",
|
|
326
|
+
"",
|
|
327
|
+
beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"]),
|
|
328
|
+
"",
|
|
329
|
+
"## Acceptance Criteria",
|
|
330
|
+
"- [ ] Preserve this | text",
|
|
331
|
+
"",
|
|
332
|
+
].join("\n"));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("preserves a following ATX heading with leading spaces", () => {
|
|
336
|
+
const before = [
|
|
337
|
+
beliefSection(["| `old-key` | 10% | 80% | old |"]),
|
|
338
|
+
"",
|
|
339
|
+
" ## Notes",
|
|
340
|
+
"Keep this text exactly.",
|
|
341
|
+
].join("\n");
|
|
342
|
+
|
|
343
|
+
expect(replaceTaskBeliefState(before, replacement)).toBe([
|
|
344
|
+
beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"]),
|
|
345
|
+
"",
|
|
346
|
+
" ## Notes",
|
|
347
|
+
"Keep this text exactly.",
|
|
348
|
+
].join("\n"));
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("inserts after Acceptance Criteria content and before the next section", () => {
|
|
352
|
+
const before = [
|
|
353
|
+
"## Objective",
|
|
354
|
+
"Ship",
|
|
355
|
+
"",
|
|
356
|
+
"## Acceptance Criteria",
|
|
357
|
+
"- [ ] Tests pass",
|
|
358
|
+
"",
|
|
359
|
+
"## Notes",
|
|
360
|
+
"Do not damage this text.",
|
|
361
|
+
].join("\n");
|
|
362
|
+
|
|
363
|
+
expect(replaceTaskBeliefState(before, replacement)).toBe([
|
|
364
|
+
"## Objective",
|
|
365
|
+
"Ship",
|
|
366
|
+
"",
|
|
367
|
+
"## Acceptance Criteria",
|
|
368
|
+
"- [ ] Tests pass",
|
|
369
|
+
"",
|
|
370
|
+
beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"]),
|
|
371
|
+
"",
|
|
372
|
+
"## Notes",
|
|
373
|
+
"Do not damage this text.",
|
|
374
|
+
].join("\n"));
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("inserts at the end when Acceptance Criteria is the final section", () => {
|
|
378
|
+
const before = "## Acceptance Criteria\n\n- [ ] Tests pass\n";
|
|
379
|
+
|
|
380
|
+
expect(replaceTaskBeliefState(before, replacement)).toBe(
|
|
381
|
+
`${before}\n${beliefSection(["| `input-valid` | 75% | 90% | A \\| B verified |"])}`,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it.each(["Input-valid", "`input-valid`", "input_valid", "input-valid\nextra"])(
|
|
386
|
+
"rejects invalid state key %j",
|
|
387
|
+
(key) => {
|
|
388
|
+
expect(() => replaceTaskBeliefState("", {
|
|
389
|
+
entries: [{ ...replacement.entries[0], key }],
|
|
390
|
+
})).toThrow("Belief keys must be kebab-case without backticks or newlines.");
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
it.each([
|
|
395
|
+
["probability", 60.5],
|
|
396
|
+
["probability", -1],
|
|
397
|
+
["probability", 101],
|
|
398
|
+
["target", 90.5],
|
|
399
|
+
["target", -1],
|
|
400
|
+
["target", 101],
|
|
401
|
+
] as const)("rejects invalid state %s %s", (field, value) => {
|
|
402
|
+
expect(() => replaceTaskBeliefState("", {
|
|
403
|
+
entries: [{ ...replacement.entries[0], [field]: value }],
|
|
404
|
+
})).toThrow(`Belief ${field} must be an integer from 0 to 100.`);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it.each(["", " ", "line one\nline two", "line one\rline two"])(
|
|
408
|
+
"rejects blank or multiline state basis %j",
|
|
409
|
+
(basis) => {
|
|
410
|
+
expect(() => replaceTaskBeliefState("", {
|
|
411
|
+
entries: [{ ...replacement.entries[0], basis }],
|
|
412
|
+
})).toThrow("Belief basis must be a nonempty single line.");
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
it("rejects duplicate state keys", () => {
|
|
417
|
+
expect(() => replaceTaskBeliefState("", {
|
|
418
|
+
entries: [replacement.entries[0], { ...replacement.entries[0], probability: 80 }],
|
|
419
|
+
})).toThrow("Belief key 'input-valid' appears more than once.");
|
|
420
|
+
});
|
|
421
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TaskFileRef, CreateTaskRequest } from "../TaskStoreProtocol";
|
|
2
|
+
import type { A2AApiKeyRecord, CreateA2AApiKeyInput } from "../A2AApiKeyStoreProtocol";
|
|
3
|
+
import type { A2AExposure } from "../A2AProtocol";
|
|
4
|
+
|
|
5
|
+
describe("A2A protocol types", () => {
|
|
6
|
+
it("TaskFileRef supports mimeType", () => {
|
|
7
|
+
const ref: TaskFileRef = { uri: "/project/uploads/a.pdf", name: "a.pdf", mimeType: "application/pdf", addedBy: "user" };
|
|
8
|
+
expect(ref.mimeType).toBe("application/pdf");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("CreateTaskRequest supports caller-provided id", () => {
|
|
12
|
+
const req: CreateTaskRequest = { id: "a2a-task-1", title: "t" };
|
|
13
|
+
expect(req.id).toBe("a2a-task-1");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("A2A key model requires projectId and supports assistantIds, no workspaceId", () => {
|
|
17
|
+
const input: CreateA2AApiKeyInput = { tenantId: "t1", projectId: "p1", assistantIds: ["a1"], label: "l" };
|
|
18
|
+
expect(input.projectId).toBe("p1");
|
|
19
|
+
const rec: A2AApiKeyRecord = {
|
|
20
|
+
id: "k1", key: "a2a_x", tenantId: "t1", projectId: "p1",
|
|
21
|
+
assistantIds: ["a1"], enabled: true, createdAt: new Date(), updatedAt: new Date(),
|
|
22
|
+
};
|
|
23
|
+
expect("workspaceId" in rec).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("A2AExposure shape", () => {
|
|
27
|
+
const exp: A2AExposure = { enabled: true, skills: [{ id: "s", name: "n", description: "d" }] };
|
|
28
|
+
expect(exp.enabled).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|