@agent-native/core 0.173.1 → 0.173.2-nightly-20260825161643
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/corpus/README.md +1 -1
- package/corpus/templates/content/actions/_database-property-input.ts +211 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +151 -26
- package/corpus/templates/content/actions/add-database-item.ts +15 -5
- package/corpus/templates/content/actions/update-database-item.ts +15 -7
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +15 -5
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/parity/README.md +11 -2
- package/corpus/templates/content/parity/eval-scenarios.ts +52 -0
- package/corpus/templates/content/parity/run-database-create-property-preservation.ts +140 -0
- package/corpus/templates/content/parity/scenario-to-eval.ts +390 -0
- package/dist/agent/run-store.js +0 -1
- package/dist/eval/agent-runner.js +23 -0
- package/dist/eval/types.d.ts +11 -0
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ PR 2.2 adds two executable tiers:
|
|
|
10
10
|
existing action tests, make no model calls, and require no private provider
|
|
11
11
|
credentials.
|
|
12
12
|
- Gated agent evals run through `agent-native eval parity`. They are opt-in via
|
|
13
|
-
`CONTENT_PARITY_EVALS=1`,
|
|
13
|
+
`CONTENT_PARITY_EVALS=1`, kept to a small explicit scenario set, and should be
|
|
14
14
|
reserved for manual or nightly checks.
|
|
15
15
|
|
|
16
16
|
## Deterministic Checks
|
|
@@ -33,14 +33,23 @@ cd templates/content
|
|
|
33
33
|
CONTENT_PARITY_EVALS=1 ANTHROPIC_API_KEY=... ./node_modules/.bin/agent-native eval parity
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
The database-create property-preservation regression has a dedicated
|
|
37
|
+
fixture-only runner so it can inspect model-produced arguments without loading
|
|
38
|
+
or executing unrelated Content actions:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
CONTENT_PARITY_EVALS=1 pnpm eval:property-preservation
|
|
42
|
+
```
|
|
43
|
+
|
|
36
44
|
With `CONTENT_PARITY_EVALS` unset, parity evals return skipped rows and do not
|
|
37
45
|
call the agent runner. The CLI still exits `0`, but both readable and JSON
|
|
38
46
|
reports mark each row with `status: "skipped"` and a `skipReason` such as
|
|
39
47
|
`Skipped because CONTENT_PARITY_EVALS is unset`.
|
|
40
48
|
|
|
41
|
-
With the gate set, the eval files
|
|
49
|
+
With the gate set, the eval files include these scenarios:
|
|
42
50
|
|
|
43
51
|
- `database-source-scope`
|
|
52
|
+
- `database-create-property-preservation`
|
|
44
53
|
- `document-search-edit`
|
|
45
54
|
- `local-file-source-truth`
|
|
46
55
|
- `builder-source-review-readonly`
|
|
@@ -8,9 +8,61 @@ export interface ParityEvalScenario {
|
|
|
8
8
|
prompt: string;
|
|
9
9
|
successSignals: string[];
|
|
10
10
|
expectedTools?: string[];
|
|
11
|
+
expectedPropertyValues?: Record<string, unknown>;
|
|
12
|
+
expectedPropertyTypes?: Record<string, string>;
|
|
13
|
+
expectedCreateEnvelope?: {
|
|
14
|
+
target: {
|
|
15
|
+
spaceId: string;
|
|
16
|
+
databaseId: string;
|
|
17
|
+
databaseDocumentId: string;
|
|
18
|
+
};
|
|
19
|
+
expectedSchemaRevision: string;
|
|
20
|
+
idempotencyKey: string;
|
|
21
|
+
title: string;
|
|
22
|
+
};
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
export const parityEvalScenarios: ParityEvalScenario[] = [
|
|
26
|
+
{
|
|
27
|
+
id: "database-create-property-preservation",
|
|
28
|
+
title: "Database create property preservation",
|
|
29
|
+
capabilityIds: ["database.rows"],
|
|
30
|
+
gateEnv: "CONTENT_PARITY_EVALS",
|
|
31
|
+
defaultState: "skipped",
|
|
32
|
+
requiresPrivateCredentials: false,
|
|
33
|
+
prompt:
|
|
34
|
+
"Find the fixture Content database titled PR #3314 feedback — do not treat 3314 as its database ID. Discover its exact database target and mutation contract through Content actions, then create exactly one row titled [FIXTURE] preserve explicit properties with idempotency key fixture-create-property-preservation-v1. Set Status to status-cannot-verify and Evidence to Baseline fixture preserve-me using the discovered immutable property IDs and property types. Omit authority identity because the authenticated server owns it. No Blocks value was requested. Call add-database-item once, then report its result truthfully.",
|
|
35
|
+
successSignals: [
|
|
36
|
+
"Discovers the exact database and mutation contract before creating.",
|
|
37
|
+
"Uses add-database-item once for the discovered stable target without authoring authority identity.",
|
|
38
|
+
"Preserves both explicitly requested writable property values.",
|
|
39
|
+
"Does not invent a Blocks value or another property.",
|
|
40
|
+
"Reports an action failure rather than claiming a row was created if the fixture is unavailable.",
|
|
41
|
+
],
|
|
42
|
+
expectedTools: [
|
|
43
|
+
"list-content-databases",
|
|
44
|
+
"get-content-database",
|
|
45
|
+
"add-database-item",
|
|
46
|
+
],
|
|
47
|
+
expectedPropertyValues: {
|
|
48
|
+
fixture_status_property: "status-cannot-verify",
|
|
49
|
+
fixture_evidence_property: "Baseline fixture preserve-me",
|
|
50
|
+
},
|
|
51
|
+
expectedPropertyTypes: {
|
|
52
|
+
fixture_status_property: "status",
|
|
53
|
+
fixture_evidence_property: "text",
|
|
54
|
+
},
|
|
55
|
+
expectedCreateEnvelope: {
|
|
56
|
+
target: {
|
|
57
|
+
spaceId: "fixture_personal_space",
|
|
58
|
+
databaseId: "fixture_feedback_database",
|
|
59
|
+
databaseDocumentId: "fixture_feedback_document",
|
|
60
|
+
},
|
|
61
|
+
expectedSchemaRevision: "fixture_schema_revision",
|
|
62
|
+
idempotencyKey: "fixture-create-property-preservation-v1",
|
|
63
|
+
title: "[FIXTURE] preserve explicit properties",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
14
66
|
{
|
|
15
67
|
id: "database-bulk-row-reliability",
|
|
16
68
|
title: "Bulk database row reliability",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createAgentRunner, runEvals } from "@agent-native/core/eval";
|
|
2
|
+
|
|
3
|
+
import addDatabaseItem from "../actions/add-database-item.ts";
|
|
4
|
+
import getContentDatabase from "../actions/get-content-database.ts";
|
|
5
|
+
import listContentDatabases from "../actions/list-content-databases.ts";
|
|
6
|
+
import { parityEvalScenarios } from "./eval-scenarios.ts";
|
|
7
|
+
import { scenarioToEval } from "./scenario-to-eval.ts";
|
|
8
|
+
|
|
9
|
+
const scenario = parityEvalScenarios.find(
|
|
10
|
+
(candidate) => candidate.id === "database-create-property-preservation",
|
|
11
|
+
);
|
|
12
|
+
if (!scenario) {
|
|
13
|
+
throw new Error("Missing database create property preservation scenario.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FIXTURE_SPACE_ID = "fixture_personal_space";
|
|
17
|
+
const FIXTURE_DATABASE_ID = "fixture_feedback_database";
|
|
18
|
+
const FIXTURE_DOCUMENT_ID = "fixture_feedback_document";
|
|
19
|
+
const FIXTURE_SCHEMA_REVISION = "fixture_schema_revision";
|
|
20
|
+
const FIXTURE_DATABASE_TITLE = "PR #3314 feedback";
|
|
21
|
+
|
|
22
|
+
const evalCase = scenarioToEval(scenario);
|
|
23
|
+
evalCase.scorers = evalCase.scorers.filter(
|
|
24
|
+
(scorer) =>
|
|
25
|
+
scorer.name === "expected_tools" ||
|
|
26
|
+
scorer.name === "expected_property_values",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const runner = await createAgentRunner({
|
|
30
|
+
actions: {
|
|
31
|
+
"list-content-databases": {
|
|
32
|
+
...listContentDatabases,
|
|
33
|
+
run: async () => ({
|
|
34
|
+
databases: [
|
|
35
|
+
{
|
|
36
|
+
databaseId: FIXTURE_DATABASE_ID,
|
|
37
|
+
documentId: FIXTURE_DOCUMENT_ID,
|
|
38
|
+
spaceId: FIXTURE_SPACE_ID,
|
|
39
|
+
title: FIXTURE_DATABASE_TITLE,
|
|
40
|
+
description: "",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
pagination: {
|
|
44
|
+
offset: 0,
|
|
45
|
+
limit: 50,
|
|
46
|
+
totalItems: 1,
|
|
47
|
+
returnedItems: 1,
|
|
48
|
+
hasMore: false,
|
|
49
|
+
nextOffset: null,
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
"get-content-database": {
|
|
54
|
+
...getContentDatabase,
|
|
55
|
+
run: async () => ({
|
|
56
|
+
database: {
|
|
57
|
+
id: FIXTURE_DATABASE_ID,
|
|
58
|
+
documentId: FIXTURE_DOCUMENT_ID,
|
|
59
|
+
spaceId: FIXTURE_SPACE_ID,
|
|
60
|
+
title: FIXTURE_DATABASE_TITLE,
|
|
61
|
+
naturalKeyPropertyId: null,
|
|
62
|
+
viewConfig: {
|
|
63
|
+
activeViewId: "fixture_view",
|
|
64
|
+
views: [],
|
|
65
|
+
sorts: [],
|
|
66
|
+
filters: [],
|
|
67
|
+
columnWidths: {},
|
|
68
|
+
},
|
|
69
|
+
createdAt: new Date(0).toISOString(),
|
|
70
|
+
updatedAt: new Date(0).toISOString(),
|
|
71
|
+
},
|
|
72
|
+
properties: [],
|
|
73
|
+
items: [],
|
|
74
|
+
source: null,
|
|
75
|
+
mutationContract: {
|
|
76
|
+
target: {
|
|
77
|
+
authorityScope: {
|
|
78
|
+
kind: "personal",
|
|
79
|
+
id: "fixture-owner@example.com",
|
|
80
|
+
},
|
|
81
|
+
spaceId: FIXTURE_SPACE_ID,
|
|
82
|
+
databaseId: FIXTURE_DATABASE_ID,
|
|
83
|
+
databaseDocumentId: FIXTURE_DOCUMENT_ID,
|
|
84
|
+
},
|
|
85
|
+
schemaRevision: FIXTURE_SCHEMA_REVISION,
|
|
86
|
+
naturalKeyPropertyId: null,
|
|
87
|
+
properties: [
|
|
88
|
+
{
|
|
89
|
+
id: "fixture_status_property",
|
|
90
|
+
name: "Status",
|
|
91
|
+
type: "status",
|
|
92
|
+
writable: true,
|
|
93
|
+
sourceManaged: false,
|
|
94
|
+
acceptedShape: null,
|
|
95
|
+
options: {
|
|
96
|
+
options: [
|
|
97
|
+
{
|
|
98
|
+
id: "status-cannot-verify",
|
|
99
|
+
name: "Cannot verify",
|
|
100
|
+
color: "gray",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "fixture_evidence_property",
|
|
107
|
+
name: "Evidence",
|
|
108
|
+
type: "text",
|
|
109
|
+
writable: true,
|
|
110
|
+
sourceManaged: false,
|
|
111
|
+
acceptedShape: null,
|
|
112
|
+
options: {},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
"add-database-item": {
|
|
119
|
+
...addDatabaseItem,
|
|
120
|
+
run: async (input) => ({ fixtureOnly: true, received: input }),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
systemPrompt:
|
|
124
|
+
"You are Content's AI document assistant. Use the registered Content actions and preserve exact user-supplied target constraints, property IDs, and property values. Never invent fields or claim an action succeeded when it failed.",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const report = await runEvals([evalCase], runner, { persist: false });
|
|
128
|
+
console.log(
|
|
129
|
+
JSON.stringify(
|
|
130
|
+
{
|
|
131
|
+
engine: runner.engine.name,
|
|
132
|
+
model: runner.model,
|
|
133
|
+
report,
|
|
134
|
+
},
|
|
135
|
+
null,
|
|
136
|
+
2,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
process.exitCode = report.failed === 0 ? 0 : 1;
|
|
@@ -30,6 +30,387 @@ function expectedToolScorer(expectedTools: string[]) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function hasExactKeys(
|
|
34
|
+
record: Record<string, unknown>,
|
|
35
|
+
expectedKeys: readonly string[],
|
|
36
|
+
): boolean {
|
|
37
|
+
const actualKeys = Object.keys(record).sort();
|
|
38
|
+
const sortedExpectedKeys = [...expectedKeys].sort();
|
|
39
|
+
return (
|
|
40
|
+
actualKeys.length === sortedExpectedKeys.length &&
|
|
41
|
+
actualKeys.every((key, index) => key === sortedExpectedKeys[index])
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function analyzePropertyValues(input: unknown): {
|
|
46
|
+
received: Record<string, unknown>;
|
|
47
|
+
receivedTypes: Record<string, string>;
|
|
48
|
+
invalid: string[];
|
|
49
|
+
} {
|
|
50
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
51
|
+
return {
|
|
52
|
+
received: {},
|
|
53
|
+
receivedTypes: {},
|
|
54
|
+
invalid: ["tool input is not an object"],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const record = input as Record<string, unknown>;
|
|
58
|
+
const hasEntries = record.propertyEntries !== undefined;
|
|
59
|
+
const hasValues = record.propertyValues !== undefined;
|
|
60
|
+
if (hasEntries && hasValues) {
|
|
61
|
+
return {
|
|
62
|
+
received: {},
|
|
63
|
+
receivedTypes: {},
|
|
64
|
+
invalid: ["propertyEntries and propertyValues were both provided"],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (hasValues) {
|
|
68
|
+
if (
|
|
69
|
+
!record.propertyValues ||
|
|
70
|
+
typeof record.propertyValues !== "object" ||
|
|
71
|
+
Array.isArray(record.propertyValues)
|
|
72
|
+
) {
|
|
73
|
+
return {
|
|
74
|
+
received: {},
|
|
75
|
+
receivedTypes: {},
|
|
76
|
+
invalid: ["propertyValues is not a record"],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
received: record.propertyValues as Record<string, unknown>,
|
|
81
|
+
receivedTypes: {},
|
|
82
|
+
invalid: ["propertyValues bypassed the typed agent input"],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!hasEntries || !Array.isArray(record.propertyEntries)) {
|
|
86
|
+
return {
|
|
87
|
+
received: {},
|
|
88
|
+
receivedTypes: {},
|
|
89
|
+
invalid: ["propertyEntries is not an array"],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const received: Record<string, unknown> = Object.create(null) as Record<
|
|
94
|
+
string,
|
|
95
|
+
unknown
|
|
96
|
+
>;
|
|
97
|
+
const receivedTypes: Record<string, string> = Object.create(null) as Record<
|
|
98
|
+
string,
|
|
99
|
+
string
|
|
100
|
+
>;
|
|
101
|
+
const invalid: string[] = [];
|
|
102
|
+
for (const entry of record.propertyEntries) {
|
|
103
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
104
|
+
invalid.push("propertyEntries contains a non-object entry");
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const { propertyId, propertyType, value } = entry as Record<
|
|
108
|
+
string,
|
|
109
|
+
unknown
|
|
110
|
+
>;
|
|
111
|
+
if (
|
|
112
|
+
!hasExactKeys(entry as Record<string, unknown>, [
|
|
113
|
+
"propertyId",
|
|
114
|
+
"propertyType",
|
|
115
|
+
"value",
|
|
116
|
+
])
|
|
117
|
+
) {
|
|
118
|
+
invalid.push(
|
|
119
|
+
"propertyEntries contains an entry with unrecognized fields",
|
|
120
|
+
);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (typeof propertyId !== "string" || propertyId.length === 0) {
|
|
124
|
+
invalid.push("propertyEntries contains an invalid propertyId");
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof propertyType !== "string" || propertyType.length === 0) {
|
|
128
|
+
invalid.push(
|
|
129
|
+
`propertyEntries contains an invalid type for ${propertyId}`,
|
|
130
|
+
);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (Object.prototype.hasOwnProperty.call(received, propertyId)) {
|
|
134
|
+
invalid.push(`propertyEntries contains duplicate ID ${propertyId}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
received[propertyId] = value;
|
|
138
|
+
receivedTypes[propertyId] = propertyType;
|
|
139
|
+
}
|
|
140
|
+
return { received, receivedTypes, invalid };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const databaseRowMutationTools = new Set([
|
|
144
|
+
"add-database-item",
|
|
145
|
+
"update-database-item",
|
|
146
|
+
"upsert-database-item-by-key",
|
|
147
|
+
"duplicate-database-items",
|
|
148
|
+
"remove-database-items",
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
function matchesCreateEnvelope(
|
|
152
|
+
input: Record<string, unknown>,
|
|
153
|
+
expected: NonNullable<ParityEvalScenario["expectedCreateEnvelope"]>,
|
|
154
|
+
): boolean {
|
|
155
|
+
const target = input.target;
|
|
156
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const actualTarget = target as Record<string, unknown>;
|
|
160
|
+
return (
|
|
161
|
+
hasExactKeys(input, [
|
|
162
|
+
"target",
|
|
163
|
+
"expectedSchemaRevision",
|
|
164
|
+
"idempotencyKey",
|
|
165
|
+
"title",
|
|
166
|
+
input.propertyEntries === undefined
|
|
167
|
+
? "propertyValues"
|
|
168
|
+
: "propertyEntries",
|
|
169
|
+
]) &&
|
|
170
|
+
hasExactKeys(actualTarget, [
|
|
171
|
+
"spaceId",
|
|
172
|
+
"databaseId",
|
|
173
|
+
"databaseDocumentId",
|
|
174
|
+
]) &&
|
|
175
|
+
actualTarget.spaceId === expected.target.spaceId &&
|
|
176
|
+
actualTarget.databaseId === expected.target.databaseId &&
|
|
177
|
+
actualTarget.databaseDocumentId === expected.target.databaseDocumentId &&
|
|
178
|
+
input.expectedSchemaRevision === expected.expectedSchemaRevision &&
|
|
179
|
+
input.idempotencyKey === expected.idempotencyKey &&
|
|
180
|
+
input.title === expected.title
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseToolResultJson(result: string | undefined): unknown {
|
|
185
|
+
if (!result) return undefined;
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(result);
|
|
188
|
+
} catch {
|
|
189
|
+
// coercion-ok: an unparseable tool result fails the same "did not supply
|
|
190
|
+
// the expected target/contract" scorer checks below as a missing one —
|
|
191
|
+
// both are non-passing evidence, not a distinction the eval needs to make.
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function expectedPropertyValuesScorer(
|
|
197
|
+
expected: Record<string, unknown>,
|
|
198
|
+
expectedTypes: Record<string, string> | undefined,
|
|
199
|
+
expectedEnvelope?: ParityEvalScenario["expectedCreateEnvelope"],
|
|
200
|
+
) {
|
|
201
|
+
return createScorer<
|
|
202
|
+
AgentRunOutput,
|
|
203
|
+
{
|
|
204
|
+
received: Record<string, unknown>;
|
|
205
|
+
missing: string[];
|
|
206
|
+
unexpected: string[];
|
|
207
|
+
invalid: string[];
|
|
208
|
+
mutationCalls: string[];
|
|
209
|
+
}
|
|
210
|
+
>({
|
|
211
|
+
name: "expected_property_values",
|
|
212
|
+
analyze(run) {
|
|
213
|
+
const mutationCalls = (run.toolCallDetails ?? [])
|
|
214
|
+
.filter((call) => databaseRowMutationTools.has(call.name))
|
|
215
|
+
.map((call) => call.name);
|
|
216
|
+
const createCalls = (run.toolCallDetails ?? []).filter(
|
|
217
|
+
(call) => call.name === "add-database-item",
|
|
218
|
+
);
|
|
219
|
+
const analysis = analyzePropertyValues(createCalls[0]?.input);
|
|
220
|
+
const invalid = [...analysis.invalid];
|
|
221
|
+
if (createCalls.length !== 1) {
|
|
222
|
+
invalid.push(
|
|
223
|
+
`expected exactly one add-database-item call, received ${createCalls.length}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (mutationCalls.length !== 1) {
|
|
227
|
+
invalid.push(
|
|
228
|
+
`expected exactly one row mutation, received ${mutationCalls.length}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const orderedCalls = run.toolCalls;
|
|
232
|
+
const listIndex = orderedCalls.indexOf("list-content-databases");
|
|
233
|
+
const inspectIndex = orderedCalls.indexOf("get-content-database");
|
|
234
|
+
const createIndex = orderedCalls.indexOf("add-database-item");
|
|
235
|
+
if (
|
|
236
|
+
listIndex < 0 ||
|
|
237
|
+
inspectIndex <= listIndex ||
|
|
238
|
+
createIndex <= inspectIndex
|
|
239
|
+
) {
|
|
240
|
+
invalid.push("database discovery did not precede the create mutation");
|
|
241
|
+
}
|
|
242
|
+
const listCall = (run.toolCallDetails ?? []).find(
|
|
243
|
+
(call) => call.name === "list-content-databases",
|
|
244
|
+
);
|
|
245
|
+
const inspectCall = (run.toolCallDetails ?? []).find(
|
|
246
|
+
(call) => call.name === "get-content-database",
|
|
247
|
+
);
|
|
248
|
+
const createCall = createCalls[0];
|
|
249
|
+
const listInput = listCall?.input as Record<string, unknown> | undefined;
|
|
250
|
+
const inspectInput = inspectCall?.input as
|
|
251
|
+
| Record<string, unknown>
|
|
252
|
+
| undefined;
|
|
253
|
+
if (
|
|
254
|
+
expectedEnvelope &&
|
|
255
|
+
(listInput?.title !== "PR #3314 feedback" ||
|
|
256
|
+
inspectInput?.databaseId !== expectedEnvelope.target.databaseId)
|
|
257
|
+
) {
|
|
258
|
+
invalid.push(
|
|
259
|
+
"discovery did not resolve the requested title to the exact create target",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
!listCall?.completed ||
|
|
264
|
+
listCall.isError ||
|
|
265
|
+
!inspectCall?.completed ||
|
|
266
|
+
inspectCall.isError
|
|
267
|
+
) {
|
|
268
|
+
invalid.push("database discovery calls did not complete successfully");
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
listCall?.completedAtEventIndex === undefined ||
|
|
272
|
+
inspectCall?.startedAtEventIndex === undefined ||
|
|
273
|
+
listCall.completedAtEventIndex >= inspectCall.startedAtEventIndex ||
|
|
274
|
+
inspectCall.completedAtEventIndex === undefined ||
|
|
275
|
+
createCall?.startedAtEventIndex === undefined ||
|
|
276
|
+
inspectCall.completedAtEventIndex >= createCall.startedAtEventIndex
|
|
277
|
+
) {
|
|
278
|
+
invalid.push(
|
|
279
|
+
"database discovery results were not available before dependent calls started",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (expectedEnvelope) {
|
|
283
|
+
const listResult = parseToolResultJson(listCall?.result) as
|
|
284
|
+
| { databases?: Array<Record<string, unknown>> }
|
|
285
|
+
| undefined;
|
|
286
|
+
const discoveredDatabase = listResult?.databases?.find(
|
|
287
|
+
(database) =>
|
|
288
|
+
database.databaseId === expectedEnvelope.target.databaseId,
|
|
289
|
+
);
|
|
290
|
+
const discoveredDocumentId =
|
|
291
|
+
discoveredDatabase?.documentId ??
|
|
292
|
+
discoveredDatabase?.databaseDocumentId;
|
|
293
|
+
if (
|
|
294
|
+
!discoveredDatabase ||
|
|
295
|
+
discoveredDocumentId !== expectedEnvelope.target.databaseDocumentId ||
|
|
296
|
+
discoveredDatabase.spaceId !== expectedEnvelope.target.spaceId
|
|
297
|
+
) {
|
|
298
|
+
invalid.push(
|
|
299
|
+
"list-content-databases result did not supply the expected create target",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const inspectResult = parseToolResultJson(inspectCall?.result) as
|
|
303
|
+
| {
|
|
304
|
+
mutationContract?: {
|
|
305
|
+
target?: Record<string, unknown>;
|
|
306
|
+
schemaRevision?: string;
|
|
307
|
+
expectedSchemaRevision?: string;
|
|
308
|
+
properties?: Array<Record<string, unknown>>;
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
| undefined;
|
|
312
|
+
const contractTarget = inspectResult?.mutationContract?.target;
|
|
313
|
+
const contractSchemaRevision =
|
|
314
|
+
inspectResult?.mutationContract?.schemaRevision ??
|
|
315
|
+
inspectResult?.mutationContract?.expectedSchemaRevision;
|
|
316
|
+
if (
|
|
317
|
+
!contractTarget ||
|
|
318
|
+
contractTarget.spaceId !== expectedEnvelope.target.spaceId ||
|
|
319
|
+
contractTarget.databaseId !== expectedEnvelope.target.databaseId ||
|
|
320
|
+
contractTarget.databaseDocumentId !==
|
|
321
|
+
expectedEnvelope.target.databaseDocumentId ||
|
|
322
|
+
contractSchemaRevision !== expectedEnvelope.expectedSchemaRevision
|
|
323
|
+
) {
|
|
324
|
+
invalid.push(
|
|
325
|
+
"get-content-database result did not supply the expected mutation contract",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const discoveredProperties = new Map(
|
|
329
|
+
(inspectResult?.mutationContract?.properties ?? []).map(
|
|
330
|
+
(property) => [property.id, property],
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
for (const [propertyId, expectedType] of Object.entries(
|
|
334
|
+
expectedTypes ?? {},
|
|
335
|
+
)) {
|
|
336
|
+
const property = discoveredProperties.get(propertyId);
|
|
337
|
+
if (
|
|
338
|
+
property?.type !== expectedType ||
|
|
339
|
+
property.writable !== true ||
|
|
340
|
+
property.sourceManaged === true
|
|
341
|
+
) {
|
|
342
|
+
invalid.push(
|
|
343
|
+
`discovery did not supply writable property ${propertyId} with type ${expectedType}`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const [propertyId, expectedType] of Object.entries(
|
|
349
|
+
expectedTypes ?? {},
|
|
350
|
+
)) {
|
|
351
|
+
if (analysis.receivedTypes[propertyId] !== expectedType) {
|
|
352
|
+
invalid.push(
|
|
353
|
+
`property ${propertyId} did not declare discovered type ${expectedType}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const createInput = createCalls[0]?.input;
|
|
358
|
+
if (
|
|
359
|
+
expectedEnvelope &&
|
|
360
|
+
(!createInput ||
|
|
361
|
+
typeof createInput !== "object" ||
|
|
362
|
+
Array.isArray(createInput) ||
|
|
363
|
+
!matchesCreateEnvelope(
|
|
364
|
+
createInput as Record<string, unknown>,
|
|
365
|
+
expectedEnvelope,
|
|
366
|
+
))
|
|
367
|
+
) {
|
|
368
|
+
invalid.push(
|
|
369
|
+
"create target, schema revision, idempotency key, or title did not match the fixture",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
!createCalls[0]?.completed ||
|
|
374
|
+
createCalls[0]?.completedSideEffect !== true ||
|
|
375
|
+
createCalls[0]?.isError
|
|
376
|
+
) {
|
|
377
|
+
invalid.push("add-database-item did not complete successfully");
|
|
378
|
+
}
|
|
379
|
+
if (!run.ok) {
|
|
380
|
+
invalid.push("agent run did not complete successfully");
|
|
381
|
+
}
|
|
382
|
+
const received = analysis.received;
|
|
383
|
+
const missing = Object.entries(expected)
|
|
384
|
+
.filter(([propertyId, value]) => received[propertyId] !== value)
|
|
385
|
+
.map(([propertyId]) => propertyId);
|
|
386
|
+
const unexpected = Object.keys(received).filter(
|
|
387
|
+
(propertyId) =>
|
|
388
|
+
!Object.prototype.hasOwnProperty.call(expected, propertyId),
|
|
389
|
+
);
|
|
390
|
+
return { received, missing, unexpected, invalid, mutationCalls };
|
|
391
|
+
},
|
|
392
|
+
generateScore({ missing, unexpected, invalid }) {
|
|
393
|
+
return missing.length === 0 &&
|
|
394
|
+
unexpected.length === 0 &&
|
|
395
|
+
invalid.length === 0
|
|
396
|
+
? 1
|
|
397
|
+
: 0;
|
|
398
|
+
},
|
|
399
|
+
generateReason({
|
|
400
|
+
analysis: { received, missing, unexpected, invalid, mutationCalls },
|
|
401
|
+
}) {
|
|
402
|
+
if (
|
|
403
|
+
missing.length === 0 &&
|
|
404
|
+
unexpected.length === 0 &&
|
|
405
|
+
invalid.length === 0
|
|
406
|
+
) {
|
|
407
|
+
return "Agent preserved every expected property ID and exact value without inventing another property.";
|
|
408
|
+
}
|
|
409
|
+
return `Received propertyValues ${JSON.stringify(received)}; mutations: ${mutationCalls.join(", ") || "none"}; missing or changed: ${missing.join(", ") || "none"}; unexpected: ${unexpected.join(", ") || "none"}; invalid: ${invalid.join("; ") || "none"}`;
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
33
414
|
export function scenarioToEval(scenario: ParityEvalScenario): Eval {
|
|
34
415
|
const name = `content-parity:${scenario.id}`;
|
|
35
416
|
|
|
@@ -52,6 +433,15 @@ export function scenarioToEval(scenario: ParityEvalScenario): Eval {
|
|
|
52
433
|
...(scenario.expectedTools?.length
|
|
53
434
|
? [expectedToolScorer(scenario.expectedTools)]
|
|
54
435
|
: []),
|
|
436
|
+
...(scenario.expectedPropertyValues
|
|
437
|
+
? [
|
|
438
|
+
expectedPropertyValuesScorer(
|
|
439
|
+
scenario.expectedPropertyValues,
|
|
440
|
+
scenario.expectedPropertyTypes,
|
|
441
|
+
scenario.expectedCreateEnvelope,
|
|
442
|
+
),
|
|
443
|
+
]
|
|
444
|
+
: []),
|
|
55
445
|
],
|
|
56
446
|
});
|
|
57
447
|
}
|
package/dist/agent/run-store.js
CHANGED
|
@@ -2246,7 +2246,6 @@ export function resolveErroredRunTerminalEvent(run) {
|
|
|
2246
2246
|
type: "error",
|
|
2247
2247
|
error: detail || "The agent run failed.",
|
|
2248
2248
|
...(code && code !== "unknown" ? { errorCode: code } : {}),
|
|
2249
|
-
recoverable: true,
|
|
2250
2249
|
},
|
|
2251
2250
|
shouldPersist: true,
|
|
2252
2251
|
};
|
|
@@ -52,19 +52,41 @@ export async function createAgentRunner(config) {
|
|
|
52
52
|
const messages = toEngineMessages(input);
|
|
53
53
|
let text = "";
|
|
54
54
|
const toolCalls = [];
|
|
55
|
+
const toolCallDetails = [];
|
|
55
56
|
let ok = true;
|
|
56
57
|
let error;
|
|
58
|
+
let eventIndex = 0;
|
|
57
59
|
const controller = new AbortController();
|
|
58
60
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
61
|
const started = Date.now();
|
|
60
62
|
const send = (event) => {
|
|
63
|
+
const currentEventIndex = eventIndex++;
|
|
61
64
|
switch (event.type) {
|
|
62
65
|
case "text":
|
|
63
66
|
text += event.text;
|
|
64
67
|
break;
|
|
65
68
|
case "tool_start":
|
|
66
69
|
toolCalls.push(event.tool);
|
|
70
|
+
toolCallDetails.push({
|
|
71
|
+
name: event.tool,
|
|
72
|
+
id: event.id,
|
|
73
|
+
input: event.input,
|
|
74
|
+
startedAtEventIndex: currentEventIndex,
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
case "tool_done": {
|
|
78
|
+
const detail = event.id
|
|
79
|
+
? toolCallDetails.find((call) => call.id === event.id)
|
|
80
|
+
: toolCallDetails.find((call) => call.name === event.tool && !call.completed);
|
|
81
|
+
if (detail) {
|
|
82
|
+
detail.completed = true;
|
|
83
|
+
detail.completedAtEventIndex = currentEventIndex;
|
|
84
|
+
detail.completedSideEffect = event.completedSideEffect;
|
|
85
|
+
detail.isError = event.isError === true;
|
|
86
|
+
detail.result = event.result;
|
|
87
|
+
}
|
|
67
88
|
break;
|
|
89
|
+
}
|
|
68
90
|
case "error":
|
|
69
91
|
ok = false;
|
|
70
92
|
error = event.error;
|
|
@@ -95,6 +117,7 @@ export async function createAgentRunner(config) {
|
|
|
95
117
|
return {
|
|
96
118
|
text,
|
|
97
119
|
toolCalls,
|
|
120
|
+
toolCallDetails: toolCallDetails.map(({ id: _id, ...detail }) => detail),
|
|
98
121
|
ok,
|
|
99
122
|
error,
|
|
100
123
|
runId,
|