@openpond/evals 0.12.3 → 0.12.5

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.
Files changed (53) hide show
  1. package/LEARNING.md +43 -0
  2. package/dist/javascript-isolate-process-entry.js +4 -1
  3. package/dist/javascript-isolate-process-source.js +1 -1
  4. package/dist/javascript-isolate-process.js +36 -2
  5. package/dist/javascript-isolate.js +8 -2
  6. package/dist/javascript-verifier-node.js +1 -0
  7. package/dist/javascript-verifier-worker-source.js +1 -1
  8. package/dist/learning/index.js +1 -0
  9. package/dist/learning/iteration-dispatch-commands.js +1 -1
  10. package/dist/learning/iteration-dispatch-contracts.js +27 -0
  11. package/dist/learning/iteration-dispatch-deferral.js +28 -0
  12. package/dist/learning/iteration-dispatch-worker.js +34 -5
  13. package/dist/tool-learning/actions.js +62 -0
  14. package/dist/tool-learning/consultation.js +198 -0
  15. package/dist/tool-learning/episodes.js +284 -0
  16. package/dist/tool-learning/index.js +6 -0
  17. package/dist/tool-learning/installations.js +151 -0
  18. package/dist/tool-learning/policy.js +38 -0
  19. package/dist/tool-learning/primitives.js +87 -0
  20. package/dist/types/javascript-isolate-process.d.ts +7 -0
  21. package/dist/types/javascript-isolate-process.d.ts.map +1 -1
  22. package/dist/types/javascript-isolate.d.ts +3 -1
  23. package/dist/types/javascript-isolate.d.ts.map +1 -1
  24. package/dist/types/javascript-verifier-node.d.ts +1 -0
  25. package/dist/types/javascript-verifier-node.d.ts.map +1 -1
  26. package/dist/types/learning/index.d.ts +1 -0
  27. package/dist/types/learning/index.d.ts.map +1 -1
  28. package/dist/types/learning/iteration-dispatch-contracts.d.ts +45 -0
  29. package/dist/types/learning/iteration-dispatch-contracts.d.ts.map +1 -1
  30. package/dist/types/learning/iteration-dispatch-deferral.d.ts +71 -0
  31. package/dist/types/learning/iteration-dispatch-deferral.d.ts.map +1 -0
  32. package/dist/types/learning/iteration-dispatch-worker.d.ts +7 -2
  33. package/dist/types/learning/iteration-dispatch-worker.d.ts.map +1 -1
  34. package/dist/types/learning/repository.d.ts +20 -0
  35. package/dist/types/learning/repository.d.ts.map +1 -1
  36. package/dist/types/learning/transport.d.ts +20 -0
  37. package/dist/types/learning/transport.d.ts.map +1 -1
  38. package/dist/types/tool-learning/actions.d.ts +54 -0
  39. package/dist/types/tool-learning/actions.d.ts.map +1 -0
  40. package/dist/types/tool-learning/consultation.d.ts +229 -0
  41. package/dist/types/tool-learning/consultation.d.ts.map +1 -0
  42. package/dist/types/tool-learning/episodes.d.ts +1058 -0
  43. package/dist/types/tool-learning/episodes.d.ts.map +1 -0
  44. package/dist/types/tool-learning/index.d.ts +7 -0
  45. package/dist/types/tool-learning/index.d.ts.map +1 -0
  46. package/dist/types/tool-learning/installations.d.ts +296 -0
  47. package/dist/types/tool-learning/installations.d.ts.map +1 -0
  48. package/dist/types/tool-learning/policy.d.ts +3 -0
  49. package/dist/types/tool-learning/policy.d.ts.map +1 -0
  50. package/dist/types/tool-learning/primitives.d.ts +73 -0
  51. package/dist/types/tool-learning/primitives.d.ts.map +1 -0
  52. package/package.json +5 -1
  53. package/schemas/learning/v1/dispatch.schema.json +100 -1
@@ -25,6 +25,7 @@ export * from "./reward-fixture-executor.js";
25
25
  export * from "./iteration-reservation-contracts.js";
26
26
  export * from "./iteration-dispatch-contracts.js";
27
27
  export * from "./iteration-dispatch-worker.js";
28
+ export * from "./iteration-dispatch-deferral.js";
28
29
  export * from "./iteration-candidate-decision.js";
29
30
  export { resolveLearningTrainingParent } from "./iteration-training-parent.js";
30
31
  export * from "./schedule-contracts.js";
@@ -22,7 +22,7 @@ export async function commandLearningIterationDispatch(tx, input, now) {
22
22
  const resumed = LearningIterationDispatchSchema.parse({ ...dispatch, revision: dispatch.revision + 1,
23
23
  state: dispatch.execution ? "submitted" : dispatch.submission ? "prepared" : "preparing",
24
24
  cancelRequestedAt: cancelling ? dispatch.cancelRequestedAt ?? now : null,
25
- consecutiveFailures: 0, nextAttemptAt: null, lastError: null, updatedAt: now });
25
+ consecutiveFailures: 0, nextAttemptAt: null, lastError: null, admission: null, updatedAt: now });
26
26
  // Preserve an in-flight lease. Its owner observes cancellation on the next
27
27
  // transition, and a replacement worker may claim only after that lease ends.
28
28
  await tx.put("dispatch", resumed, dispatch.revision, { parentId: iteration.id, status: resumed.state });
@@ -8,6 +8,24 @@ export const LearningIterationSubmissionSchema = z.object({
8
8
  protocol: ReleaseIdSchema,
9
9
  payload: LearningJsonObjectSchema,
10
10
  }).strict();
11
+ /** Normal capacity/calendar waits are not failed execution attempts. */
12
+ export const LearningIterationAdmissionSchema = z.discriminatedUnion("status", [
13
+ z.object({ status: z.literal("ready") }).strict(),
14
+ z.object({ status: z.literal("deferred"), retryAt: ReleaseTimestampSchema,
15
+ reason: z.string().trim().min(1).max(2_000) }).strict(),
16
+ ]);
17
+ /** The execution owner has durably rejected/fenced this exact attempt before
18
+ * creating an execution or spending. A timeout/absence read is not this proof. */
19
+ export const LearningIterationSubmissionDeferralSchema = z.object({
20
+ scope: ReleaseIdSchema,
21
+ dispatchId: ReleaseIdSchema,
22
+ submissionHash: z.string().regex(/^[a-f0-9]{64}$/),
23
+ leaseGeneration: z.number().int().positive(),
24
+ status: z.literal("deferred"),
25
+ retryAt: ReleaseTimestampSchema,
26
+ reason: z.string().trim().min(1).max(2_000),
27
+ receipt: ImmutableReleaseRefSchema,
28
+ }).strict();
11
29
  export const LearningIterationDispatchSchema = z.object({
12
30
  schemaVersion: z.literal("openpond.learningIterationDispatch.v1"),
13
31
  id: ReleaseIdSchema,
@@ -28,6 +46,8 @@ export const LearningIterationDispatchSchema = z.object({
28
46
  leaseExpiresAt: ReleaseTimestampSchema.nullable(),
29
47
  consecutiveFailures: z.number().int().nonnegative(),
30
48
  nextAttemptAt: ReleaseTimestampSchema.nullable(),
49
+ admission: LearningIterationAdmissionSchema.nullable().optional(),
50
+ lastSubmissionDeferral: LearningIterationSubmissionDeferralSchema.optional(),
31
51
  lastError: z.string().max(20_000).nullable(),
32
52
  createdAt: ReleaseTimestampSchema,
33
53
  updatedAt: ReleaseTimestampSchema,
@@ -37,6 +57,13 @@ export const LearningIterationDispatchSchema = z.object({
37
57
  }
38
58
  if (value.submissionStartedAt && !value.submission)
39
59
  context.addIssue({ code: "custom", path: ["submission"], message: "A started dispatch requires its persisted submission." });
60
+ if (value.admission?.status === "deferred" && (value.submissionStartedAt || value.state !== "prepared" || value.nextAttemptAt !== value.admission.retryAt)) {
61
+ context.addIssue({ code: "custom", path: ["admission"], message: "Admission deferral requires an unstarted prepared dispatch and its retry time." });
62
+ }
63
+ if (value.lastSubmissionDeferral && (value.lastSubmissionDeferral.dispatchId !== value.id ||
64
+ value.lastSubmissionDeferral.submissionHash !== value.submissionHash || value.lastSubmissionDeferral.leaseGeneration >= value.leaseGeneration)) {
65
+ context.addIssue({ code: "custom", path: ["lastSubmissionDeferral"], message: "A retained deferral must identify an earlier fenced attempt of this submission." });
66
+ }
40
67
  if (value.execution && !value.submissionStartedAt)
41
68
  context.addIssue({ code: "custom", path: ["execution"], message: "An execution requires a started dispatch." });
42
69
  if (["prepared", "submitted"].includes(value.state) && !value.submission)
@@ -0,0 +1,28 @@
1
+ import { LearningIterationSchema } from "./contracts.js";
2
+ import { LearningDomainError } from "./errors.js";
3
+ import { LearningIterationDispatchSchema, LearningIterationSubmissionDeferralSchema } from "./iteration-dispatch-contracts.js";
4
+ /** Call under the dispatch's scope lock. The execution owner must first fence
5
+ * this attempt and prove no execution/charge exists. When the owner shares this
6
+ * repository, commit this mutation in its native admission transaction instead:
7
+ * late submits then fail the same generation fence as lease replacement.
8
+ */
9
+ export async function deferLearningIterationSubmission(tx, scope, current, iteration, value, now) {
10
+ const outcome = LearningIterationSubmissionDeferralSchema.parse(value);
11
+ if (outcome.scope !== scope || outcome.dispatchId !== current.id || outcome.submissionHash !== current.submissionHash ||
12
+ outcome.leaseGeneration !== current.leaseGeneration || !current.submissionStartedAt || current.execution ||
13
+ current.state !== "prepared" || !current.leaseOwner ||
14
+ !current.leaseExpiresAt || Date.parse(current.leaseExpiresAt) <= Date.parse(now) ||
15
+ iteration.id !== current.iterationId || (iteration.status !== "dispatching" && !(current.cancelRequestedAt && iteration.status === "cancelling")))
16
+ throw new LearningDomainError("learning_submission_deferral_fenced", 409);
17
+ const retryAt = new Date(Math.max(Date.parse(outcome.retryAt), Date.parse(now) + 1_000)).toISOString();
18
+ const dispatch = LearningIterationDispatchSchema.parse({ ...current, revision: current.revision + 1,
19
+ submissionStartedAt: null, leaseGeneration: current.leaseGeneration + 1, leaseOwner: null, leaseExpiresAt: null,
20
+ admission: current.cancelRequestedAt ? null : { status: "deferred", reason: outcome.reason, retryAt },
21
+ nextAttemptAt: current.cancelRequestedAt ? null : retryAt, lastSubmissionDeferral: outcome,
22
+ consecutiveFailures: 0, lastError: null, updatedAt: now });
23
+ await tx.put("dispatch", dispatch, current.revision, { parentId: current.iterationId, status: dispatch.state });
24
+ if (iteration.failure)
25
+ await tx.put("iteration", LearningIterationSchema.parse({ ...iteration,
26
+ revision: iteration.revision + 1, failure: null, updatedAt: now }), iteration.revision, { parentId: current.chainId, status: iteration.status });
27
+ return dispatch;
28
+ }
@@ -2,8 +2,10 @@ import { contentHash } from "@openpond/harness";
2
2
  import { LearningIterationSchema } from "./contracts.js";
3
3
  import { LearningDomainError } from "./errors.js";
4
4
  import { LearningChainSchema, LearningIterationReservationSchema } from "./iteration-reservation-contracts.js";
5
- import { LearningIterationDispatchSchema, LearningIterationObservationSchema, LearningIterationSubmissionSchema, } from "./iteration-dispatch-contracts.js";
5
+ import { LearningIterationAdmissionSchema, LearningIterationDispatchSchema, LearningIterationObservationSchema, LearningIterationSubmissionSchema, } from "./iteration-dispatch-contracts.js";
6
6
  import { requireLearningRelease, requireLearningResource } from "./repository.js";
7
+ import { deferLearningIterationSubmission } from "./iteration-dispatch-deferral.js";
8
+ import { LearningIterationSubmissionDeferralSchema } from "./iteration-dispatch-contracts.js";
7
9
  const closed = new Set(["accepted", "rejected", "failed", "cancelled", "completed_without_candidate"]);
8
10
  export function createLearningIterationWorker(repository, executor, options) {
9
11
  const now = options.now ?? (() => new Date().toISOString());
@@ -20,7 +22,7 @@ export function createLearningIterationWorker(repository, executor, options) {
20
22
  const stamp = now();
21
23
  if (current?.state === "settled" || (current?.state === "blocked" && !current.cancelRequestedAt)
22
24
  || (current?.leaseExpiresAt && Date.parse(current.leaseExpiresAt) > Date.parse(stamp))
23
- || (current?.nextAttemptAt && Date.parse(current.nextAttemptAt) > Date.parse(stamp)))
25
+ || (!current?.cancelRequestedAt && current?.nextAttemptAt && Date.parse(current.nextAttemptAt) > Date.parse(stamp)))
24
26
  return null;
25
27
  const policy = await requireLearningRelease(tx, "policy", iteration.policy);
26
28
  if (policy.executionOwner !== options.executionOwner)
@@ -80,7 +82,7 @@ export function createLearningIterationWorker(repository, executor, options) {
80
82
  });
81
83
  await tx.put("iteration", updated, iteration.revision, { parentId: current.chainId, status });
82
84
  await saveDispatch(tx, current, { state: terminal ? "settled" : "submitted", execution: outcome?.execution ?? current.execution,
83
- terminalReceipt: outcome?.receipt ?? null, leaseOwner: null, leaseExpiresAt: null,
85
+ terminalReceipt: outcome?.receipt ?? null, leaseOwner: null, leaseExpiresAt: null, admission: null,
84
86
  nextAttemptAt: terminal ? null : new Date(Date.parse(now()) + 5_000).toISOString(), consecutiveFailures: 0, lastError: null });
85
87
  if (terminal) {
86
88
  const reservation = await requireLearningResource(tx, "reservation", iteration.id);
@@ -124,6 +126,25 @@ export function createLearningIterationWorker(repository, executor, options) {
124
126
  return read();
125
127
  dispatch = prepared;
126
128
  }
129
+ if (!dispatch.submissionStartedAt && !dispatch.cancelRequestedAt && executor.admit) {
130
+ const checkedAt = now();
131
+ const admission = LearningIterationAdmissionSchema.parse(await bounded(input => executor.admit(input)));
132
+ if (admission.status === "deferred") {
133
+ if (Date.parse(admission.retryAt) <= Date.parse(checkedAt))
134
+ throw new LearningDomainError("learning_admission_retry_time_invalid", 409);
135
+ await fenced(scope, dispatch, async (tx, current, iteration) => {
136
+ if (current.cancelRequestedAt)
137
+ return settle(tx, current, iteration, null);
138
+ const retryAt = new Date(Math.max(Date.parse(admission.retryAt), Date.parse(now()) + 1_000)).toISOString();
139
+ await saveDispatch(tx, current, { admission: { ...admission, retryAt }, nextAttemptAt: retryAt,
140
+ consecutiveFailures: 0, lastError: null, leaseOwner: null, leaseExpiresAt: null });
141
+ if (iteration.failure)
142
+ await tx.put("iteration", LearningIterationSchema.parse({ ...iteration,
143
+ revision: iteration.revision + 1, failure: null, updatedAt: now() }), iteration.revision, { parentId: current.chainId, status: iteration.status });
144
+ });
145
+ return read();
146
+ }
147
+ }
127
148
  const starting = await fenced(scope, dispatch, async (tx, current, iteration) => {
128
149
  if (current.cancelRequestedAt && !current.submissionStartedAt) {
129
150
  await settle(tx, current, iteration, null);
@@ -134,7 +155,7 @@ export function createLearningIterationWorker(repository, executor, options) {
134
155
  await saveDispatch(tx, current, { leaseOwner: null, leaseExpiresAt: null });
135
156
  return null;
136
157
  }
137
- return saveDispatch(tx, current, { submissionStartedAt: current.submissionStartedAt ?? now() });
158
+ return saveDispatch(tx, current, { submissionStartedAt: current.submissionStartedAt ?? now(), admission: null });
138
159
  });
139
160
  if (!starting)
140
161
  return read();
@@ -147,6 +168,14 @@ export function createLearningIterationWorker(repository, executor, options) {
147
168
  dispatch = refreshed;
148
169
  observed = dispatch.cancelRequestedAt ? await bounded(input => executor.cancel(input)) : await bounded(input => executor.submit(input));
149
170
  }
171
+ if (observed.status === "deferred") {
172
+ const outcome = LearningIterationSubmissionDeferralSchema.parse(observed);
173
+ if (outcome.scope !== scope || outcome.dispatchId !== dispatch.id || outcome.submissionHash !== dispatch.submissionHash ||
174
+ outcome.leaseGeneration !== dispatch.leaseGeneration)
175
+ throw new LearningDomainError("learning_execution_identity_mismatch", 409);
176
+ await fenced(scope, dispatch, (tx, current, iteration) => deferLearningIterationSubmission(tx, scope, current, iteration, outcome, now()));
177
+ return read();
178
+ }
150
179
  const outcome = LearningIterationObservationSchema.parse(observed);
151
180
  if (outcome.scope !== scope || outcome.dispatchId !== dispatch.id || outcome.submissionHash !== dispatch.submissionHash
152
181
  || (dispatch.execution && contentHash(outcome.execution) !== contentHash(dispatch.execution)))
@@ -165,7 +194,7 @@ export function createLearningIterationWorker(repository, executor, options) {
165
194
  const blocked = failures > acquired.policy.limits.maxRetries;
166
195
  const message = (error instanceof Error ? error.message : "Learning dispatch failed.").slice(0, 20_000);
167
196
  await saveDispatch(tx, current, { state: blocked ? "blocked" : current.state, consecutiveFailures: failures,
168
- lastError: message, leaseOwner: null, leaseExpiresAt: null,
197
+ lastError: message, leaseOwner: null, leaseExpiresAt: null, admission: null,
169
198
  nextAttemptAt: blocked ? null : new Date(Date.parse(now()) + Math.min(60_000, 1_000 * 2 ** Math.min(failures, 6))).toISOString() });
170
199
  const updated = LearningIterationSchema.parse({ ...iteration, revision: iteration.revision + 1, retryCount: iteration.retryCount + 1,
171
200
  failure: { code: blocked ? "learning_dispatch_attention_required" : "learning_dispatch_retry_pending", message }, updatedAt: now() });
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import { TOOL_LEARNING_LIMITS, ToolLearningHashSchema, boundedToolLearningSchema, toolLearningText, } from "./primitives.js";
3
+ const Text = toolLearningText(TOOL_LEARNING_LIMITS.actionBytes);
4
+ const RelativePath = z
5
+ .string()
6
+ .min(1)
7
+ .max(1_000)
8
+ .refine((value) => !value.startsWith("/") &&
9
+ !value.includes("\\") &&
10
+ !/^[A-Za-z]:/u.test(value) &&
11
+ value.split("/").every((part) => part !== "" && part !== "." && part !== ".."), "Use a workspace-relative artifact path.");
12
+ export const ToolLearningActionSchema = boundedToolLearningSchema(z.discriminatedUnion("kind", [
13
+ z.object({ kind: z.literal("python"), code: Text }).strict(),
14
+ z
15
+ .object({ kind: z.literal("shell"), command: Text, shell: z.string().min(1).max(120) })
16
+ .strict(),
17
+ z
18
+ .object({
19
+ kind: z.literal("tool_arguments"),
20
+ arguments: z.record(z.string().min(1).max(200), z.json()),
21
+ })
22
+ .strict(),
23
+ z
24
+ .object({
25
+ kind: z.literal("edit"),
26
+ patch: Text,
27
+ baseFiles: z
28
+ .array(z
29
+ .object({ path: RelativePath, contentHash: ToolLearningHashSchema.nullable() })
30
+ .strict())
31
+ .min(1)
32
+ .max(32),
33
+ })
34
+ .strict(),
35
+ ]), TOOL_LEARNING_LIMITS.actionBytes);
36
+ export const ToolLearningRepairSchema = z.discriminatedUnion("kind", [
37
+ z
38
+ .object({
39
+ kind: z.literal("replacement"),
40
+ expectedActionHash: ToolLearningHashSchema,
41
+ environmentFingerprint: ToolLearningHashSchema,
42
+ action: ToolLearningActionSchema,
43
+ })
44
+ .strict(),
45
+ z
46
+ .object({
47
+ kind: z.literal("patch"),
48
+ expectedActionHash: ToolLearningHashSchema,
49
+ environmentFingerprint: ToolLearningHashSchema,
50
+ files: z
51
+ .array(z
52
+ .object({
53
+ path: RelativePath,
54
+ expectedContentHash: ToolLearningHashSchema,
55
+ patch: Text,
56
+ })
57
+ .strict())
58
+ .min(1)
59
+ .max(16),
60
+ })
61
+ .strict(),
62
+ ]);
@@ -0,0 +1,198 @@
1
+ import { z } from "zod";
2
+ import { ToolLearningActionSchema } from "./actions.js";
3
+ import { TOOL_LEARNING_LIMITS, ToolLearningEnvironmentSchema, ToolLearningHashSchema as Hash, ToolLearningIdSchema as Id, ToolLearningRefSchema as Ref, ToolLearningScopeSchema, ToolLearningTimestampSchema as Timestamp, boundedToolLearningSchema, toolLearningContentHash, toolLearningText, } from "./primitives.js";
4
+ export const ToolLearningCapabilitySchema = z
5
+ .object({
6
+ id: Id,
7
+ application: z.string().min(1).max(200),
8
+ versions: z.array(z.string().min(1).max(200)).min(1).max(32),
9
+ tools: z.array(z.string().min(1).max(200)).min(1).max(32),
10
+ mode: z.literal("recovery"),
11
+ routingInstruction: toolLearningText(2000),
12
+ qualification: z.discriminatedUnion("kind", [
13
+ z.object({ kind: z.literal("integration"), receipt: Ref }).strict(),
14
+ z
15
+ .object({
16
+ kind: z.literal("learned"),
17
+ receipt: Ref,
18
+ trainingRun: Ref,
19
+ adapter: Ref,
20
+ evaluation: Ref,
21
+ activation: Ref,
22
+ })
23
+ .strict(),
24
+ ]),
25
+ })
26
+ .strict();
27
+ const Manifest = z
28
+ .object({
29
+ schemaVersion: z.literal("openpond.toolLearningManifest.v1"),
30
+ installationId: Id,
31
+ scope: ToolLearningScopeSchema,
32
+ revision: z.number().int().positive(),
33
+ contentHash: Hash,
34
+ issuedAt: Timestamp,
35
+ expiresAt: Timestamp,
36
+ modelRelease: Ref,
37
+ responseSchema: z.literal("openpond.toolLearningDecision.v1"),
38
+ capabilities: z.array(ToolLearningCapabilitySchema).max(TOOL_LEARNING_LIMITS.capabilities),
39
+ })
40
+ .strict()
41
+ .superRefine((manifest, context) => {
42
+ const { contentHash, ...body } = manifest;
43
+ if (toolLearningContentHash(body) !== contentHash)
44
+ context.addIssue({
45
+ code: "custom",
46
+ path: ["contentHash"],
47
+ message: "Manifest content hash mismatch.",
48
+ });
49
+ const lifetime = Date.parse(manifest.expiresAt) - Date.parse(manifest.issuedAt);
50
+ if (lifetime <= 0 || lifetime > TOOL_LEARNING_LIMITS.manifestSeconds * 1000)
51
+ context.addIssue({
52
+ code: "custom",
53
+ path: ["expiresAt"],
54
+ message: "Manifest expiry exceeds the supported lifetime.",
55
+ });
56
+ if (new Set(manifest.capabilities.map((entry) => entry.id)).size !== manifest.capabilities.length)
57
+ context.addIssue({
58
+ code: "custom",
59
+ path: ["capabilities"],
60
+ message: "Capability IDs must be unique.",
61
+ });
62
+ });
63
+ export const ToolLearningManifestSchema = boundedToolLearningSchema(Manifest, TOOL_LEARNING_LIMITS.consultationBytes);
64
+ export const ToolLearningConsultationRequestSchema = boundedToolLearningSchema(z
65
+ .object({
66
+ schemaVersion: z.literal("openpond.toolLearningConsultation.v1"),
67
+ operationId: Id,
68
+ installationId: Id,
69
+ scope: ToolLearningScopeSchema,
70
+ sessionId: Id,
71
+ callId: Id,
72
+ capabilityId: Id,
73
+ manifestHash: Hash,
74
+ modelRelease: Ref,
75
+ mode: z.literal("recovery"),
76
+ intent: toolLearningText(2000),
77
+ action: ToolLearningActionSchema,
78
+ actionHash: Hash,
79
+ environment: ToolLearningEnvironmentSchema,
80
+ contextHash: Hash,
81
+ failure: z
82
+ .object({
83
+ message: toolLearningText(TOOL_LEARNING_LIMITS.resultBytes),
84
+ observation: Ref,
85
+ })
86
+ .strict(),
87
+ round: z.number().int().min(1).max(TOOL_LEARNING_LIMITS.consultationRounds),
88
+ })
89
+ .strict()
90
+ .superRefine((request, context) => {
91
+ if (toolLearningContentHash(request.action) !== request.actionHash)
92
+ context.addIssue({
93
+ code: "custom",
94
+ path: ["actionHash"],
95
+ message: "Consultation action hash mismatch.",
96
+ });
97
+ if (toolLearningContentHash({
98
+ intent: request.intent,
99
+ environment: request.environment,
100
+ failure: request.failure,
101
+ }) !== request.contextHash)
102
+ context.addIssue({
103
+ code: "custom",
104
+ path: ["contextHash"],
105
+ message: "Consultation context hash mismatch.",
106
+ });
107
+ }), TOOL_LEARNING_LIMITS.consultationBytes);
108
+ /** Only model-authored task content; transport failures never become proceed. */
109
+ export const ToolLearningDecisionSchema = z.discriminatedUnion("decision", [
110
+ z.object({ decision: z.literal("proceed") }).strict(),
111
+ z
112
+ .object({
113
+ decision: z.literal("revise"),
114
+ issues: z
115
+ .array(z
116
+ .object({
117
+ category: z.string().min(1).max(120),
118
+ feedback: toolLearningText(2000),
119
+ })
120
+ .strict())
121
+ .min(1)
122
+ .max(8),
123
+ })
124
+ .strict(),
125
+ z
126
+ .object({
127
+ decision: z.literal("needs_context"),
128
+ missing: z
129
+ .array(z
130
+ .object({
131
+ field: z.string().min(1).max(120),
132
+ reason: toolLearningText(500),
133
+ })
134
+ .strict())
135
+ .min(1)
136
+ .max(8),
137
+ })
138
+ .strict(),
139
+ ]);
140
+ const ResponseIdentity = {
141
+ operationId: Id,
142
+ installationId: Id,
143
+ capabilityId: Id,
144
+ manifestHash: Hash,
145
+ modelRelease: Ref,
146
+ actionHash: Hash,
147
+ contextHash: Hash,
148
+ };
149
+ export const ToolLearningConsultationResponseSchema = boundedToolLearningSchema(z.discriminatedUnion("state", [
150
+ z
151
+ .object({
152
+ schemaVersion: z.literal("openpond.toolLearningDecision.v1"),
153
+ ...ResponseIdentity,
154
+ state: z.literal("completed"),
155
+ decision: ToolLearningDecisionSchema,
156
+ usageReceipt: Ref,
157
+ completedAt: Timestamp,
158
+ })
159
+ .strict(),
160
+ z
161
+ .object({
162
+ schemaVersion: z.literal("openpond.toolLearningDecision.v1"),
163
+ ...ResponseIdentity,
164
+ state: z.literal("unavailable"),
165
+ reason: z.enum([
166
+ "disabled",
167
+ "revoked",
168
+ "expired_manifest",
169
+ "unsupported_scope",
170
+ "budget_exhausted",
171
+ "timeout",
172
+ "invalid_output",
173
+ "cancelled",
174
+ "dispatch_uncertain",
175
+ ]),
176
+ usageReceipt: Ref.nullable(),
177
+ })
178
+ .strict(),
179
+ ]), TOOL_LEARNING_LIMITS.consultationBytes);
180
+ /** Invoke before exposing a result to the agent; even valid JSON may be stale. */
181
+ export function assertToolLearningConsultationResponse(requestValue, responseValue) {
182
+ const request = ToolLearningConsultationRequestSchema.parse(requestValue);
183
+ const response = ToolLearningConsultationResponseSchema.parse(responseValue);
184
+ for (const key of [
185
+ "operationId",
186
+ "installationId",
187
+ "capabilityId",
188
+ "manifestHash",
189
+ "actionHash",
190
+ "contextHash",
191
+ ])
192
+ if (request[key] !== response[key])
193
+ throw new Error(`tool_learning_consultation_${key}_mismatch`);
194
+ if (request.modelRelease.id !== response.modelRelease.id ||
195
+ request.modelRelease.contentHash !== response.modelRelease.contentHash)
196
+ throw new Error("tool_learning_consultation_model_mismatch");
197
+ return response;
198
+ }