@openpond/harness 0.2.6 → 0.4.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/CONTRACT.md CHANGED
@@ -51,3 +51,78 @@ unselected evidence remains available to a later host watermark.
51
51
  Evaluation execution and model-improvement qualification contracts that bind a
52
52
  Harness to a Taskset, scored baseline, Model, verifier, and training signal
53
53
  belong to `@openpond/evals`.
54
+
55
+ ## Released source transport
56
+
57
+ ### Profile workflow catalog
58
+
59
+ A Git-backed Profile may contain `workflows/catalog.json` beside its Skills,
60
+ Agents, and evals. The file uses `openpond.profileWorkflows.v1`:
61
+
62
+ ```json
63
+ {
64
+ "schemaVersion": "openpond.profileWorkflows.v1",
65
+ "workflows": [{
66
+ "id": "weekly-report",
67
+ "label": "Weekly report",
68
+ "description": "Summarize a supplied week.",
69
+ "inputSchema": { "type": "object", "properties": { "week": { "type": "integer" } }, "required": ["week"] },
70
+ "invocation": { "kind": "instructions", "instructions": "Write the report for the supplied week." },
71
+ "skillPaths": ["skills/report/SKILL.md"]
72
+ }]
73
+ }
74
+ ```
75
+
76
+ Workflow IDs are stable within one catalog. `skillPaths` name enabled primary
77
+ Skill files in the released Harness source. An `agent_action` invocation names
78
+ an enabled Agent action from the Profile action catalog. Import retains its
79
+ identity, input schema, and Agent source in the same immutable release; the
80
+ host executes it through the existing Agent SDK runner. Duplicate IDs, unsafe
81
+ paths, missing references, invalid input schemas, and unsupported actions fail
82
+ import. The catalog's exact bytes and generated `workflows/actions.json`
83
+ inventory are included in the Harness release. Profile eval files remain
84
+ outside model-visible Harness content.
85
+
86
+ `openpond.profileWorkflowBinding.v1` names the Profile, accepted source
87
+ revision, Harness release, catalog content hash, and workflow ID. The host
88
+ resolves it from a fully verified source package. A bound Work session uses
89
+ that release for every turn; source updates create new releases for fresh
90
+ sessions and do not change admitted runs or personal Harness selection.
91
+
92
+ Local servers expose committed workflow bindings at `GET /v1/profile/workflows`.
93
+ Create a Work session with its `currentProfile` and returned
94
+ `profileWorkflowBinding`, then start a turn with `workflowInput` matching the
95
+ workflow's `inputSchema`. The turn records its binding and input hash.
96
+
97
+ `openpond.harnessSourcePackage.v1` carries the complete immutable Agent
98
+ snapshot, Harness release and their released file bytes. Creation and readback
99
+ verify both release hashes, dependency references, the exact file population,
100
+ canonical base64, individual file hashes and a 25 MiB total byte limit.
101
+ Rehashing a transport envelope does not authorize different source bytes.
102
+ Instruction and Skill entry files must be policy-visible; verifier and
103
+ host-private assets retain their declared visibility and must never be exposed
104
+ to a policy by iterating the complete source map.
105
+
106
+ This is source transport, not runtime conformance. Hosts authorize export and
107
+ select an execution adapter that consumes the captured source, verifies its
108
+ capabilities and records effective context. A matching Harness release hash
109
+ alone does not prove that instructions, Skills or executable dependencies ran.
110
+
111
+ `createHarnessSourceRuntime` consumes captured source for the declarative
112
+ `openpond.agent-runtime.v1` program. It loads released instruction and Skill
113
+ text, verifies required capabilities, dependency versions and actual tool
114
+ schemas, and exposes policy-visible resources through bounded byte-range reads.
115
+ Private assets never enter its context or reader. Unsupported programs and
116
+ released subagents fail admission. Hosts must provide their actual tools and
117
+ capabilities; this helper does not implement missing execution capabilities.
118
+ The runtime receipt binds the source, effective system prompt, loaded assets
119
+ and tool definitions. Hosts retain that receipt with attempt evidence.
120
+
121
+ `executeHarnessRollout` owns the shared policy/environment round lifecycle,
122
+ released resource calls, retained conversation and exhaustion behavior. Hosts
123
+ supply policy transport and environment step/termination operations. Desktop's
124
+ agent runtime re-exports the same provider loop from this package.
125
+ `@openpond/harness/runtime-source` distributes a self-contained ESM build of
126
+ source admission and rollout execution with its SHA-256. Hosted archives use
127
+ those published bytes instead of implementing a second source reader or loop.
128
+ The clean consumer check executes this distribution without module resolution.
package/dist/index.js CHANGED
@@ -10,3 +10,8 @@ export * from "./refiner-detection.js";
10
10
  export * from "./refinement-lifecycle.js";
11
11
  export * from "./refiner-support.js";
12
12
  export * from "./tools.js";
13
+ export * from "./source-package.js";
14
+ export * from "./source-runtime.js";
15
+ export * from "./provider-loop.js";
16
+ export * from "./profile-workflows.js";
17
+ export * from "./source-execution.js";
@@ -0,0 +1,138 @@
1
+ import { z } from "zod";
2
+ import { contentHash, ImmutableReleaseRefSchema, ReleaseHashSchema, sha256 } from "./common.js";
3
+ import { harnessSourcePackageFiles, validateHarnessSourcePackage } from "./source-package.js";
4
+ const WorkflowIdSchema = z.string().regex(/^[a-z][a-z0-9_-]{0,119}$/);
5
+ const SourcePathSchema = z.string().min(1).max(2_000).refine((value) => !value.includes("\\") && !value.includes(":") && !value.startsWith("/")
6
+ && value.split("/").every((part) => part !== "" && part !== "." && part !== ".."), "workflow references require portable relative paths");
7
+ export const ProfileWorkflowSchema = z.object({
8
+ id: WorkflowIdSchema,
9
+ label: z.string().trim().min(1).max(240),
10
+ description: z.string().max(4_000),
11
+ inputSchema: z.record(z.string(), z.unknown()),
12
+ invocation: z.discriminatedUnion("kind", [
13
+ z.object({ kind: z.literal("instructions"), instructions: z.string().trim().min(1).max(100_000) }).strict(),
14
+ z.object({ kind: z.literal("agent_action"), actionId: z.string().trim().min(1).max(240) }).strict(),
15
+ ]),
16
+ skillPaths: z.array(SourcePathSchema).max(100),
17
+ }).strict();
18
+ export const ProfileWorkflowCatalogSchema = z.object({
19
+ schemaVersion: z.literal("openpond.profileWorkflows.v1"),
20
+ workflows: z.array(ProfileWorkflowSchema).max(1_000),
21
+ }).strict();
22
+ /** Action identities retained beside a released workflow catalog. The host
23
+ * resolves these identities to the Agent files in the same immutable release. */
24
+ export const ProfileWorkflowActionSchema = z.object({
25
+ id: z.string().trim().min(1).max(240),
26
+ agentId: z.string().regex(/^[a-zA-Z0-9_-]{1,240}$/),
27
+ sourceActionId: z.string().trim().min(1).max(240),
28
+ inputSchema: z.record(z.string(), z.unknown()),
29
+ }).strict();
30
+ export const ProfileWorkflowActionsSchema = z.object({
31
+ schemaVersion: z.literal("openpond.profileWorkflowActions.v1"),
32
+ actions: z.array(ProfileWorkflowActionSchema).max(200),
33
+ }).strict();
34
+ /** Validates references against the exact files admitted to a released source. */
35
+ export function validateProfileWorkflowCatalog(input) {
36
+ const catalog = ProfileWorkflowCatalogSchema.parse(input.catalog);
37
+ const ids = new Set();
38
+ for (const workflow of catalog.workflows) {
39
+ if (ids.has(workflow.id))
40
+ throw new Error(`Duplicate Profile workflow id ${workflow.id}.`);
41
+ ids.add(workflow.id);
42
+ for (const skillPath of workflow.skillPaths) {
43
+ if (!input.sourcePaths.has(skillPath))
44
+ throw new Error(`Profile workflow ${workflow.id} references missing Skill ${skillPath}.`);
45
+ }
46
+ if (workflow.invocation.kind === "agent_action" && !input.actionIds.has(workflow.invocation.actionId)) {
47
+ throw new Error(`Profile workflow ${workflow.id} references missing action ${workflow.invocation.actionId}.`);
48
+ }
49
+ }
50
+ return catalog;
51
+ }
52
+ export const ProfileWorkflowBindingSchema = z.object({
53
+ schemaVersion: z.literal("openpond.profileWorkflowBinding.v1"),
54
+ profileId: z.string().trim().min(1).max(240),
55
+ sourceRevision: z.string().trim().min(1).max(240),
56
+ harnessRelease: ImmutableReleaseRefSchema,
57
+ catalogHash: ReleaseHashSchema,
58
+ workflowId: WorkflowIdSchema,
59
+ }).strict();
60
+ /** Resolve a catalog only from a verified immutable source package. */
61
+ export function loadReleasedProfileWorkflowCatalog(value) {
62
+ const sourcePackage = validateHarnessSourcePackage(value);
63
+ const files = harnessSourcePackageFiles(sourcePackage);
64
+ const loaded = loadReleasedProfileWorkflowCatalogAssets({
65
+ agentSnapshot: sourcePackage.agentSnapshot,
66
+ harnessRelease: sourcePackage.harnessRelease,
67
+ catalogBytes: files.get("workflows/catalog.json"),
68
+ actionBytes: files.get("workflows/actions.json"),
69
+ });
70
+ return { sourcePackage, ...loaded };
71
+ }
72
+ /** Load only the two workflow assets after the caller has verified all files
73
+ * in a local release. This avoids packaging large Agent source each turn. */
74
+ export function loadReleasedProfileWorkflowCatalogAssets(input) {
75
+ const asset = input.harnessRelease.files.find((file) => file.path === "workflows/catalog.json");
76
+ const actionAsset = input.harnessRelease.files.find((file) => file.path === "workflows/actions.json");
77
+ if (!asset || asset.visibility !== "policy" || !input.catalogBytes || sha256(input.catalogBytes) !== asset.contentHash) {
78
+ throw new Error("Released Profile workflow catalog is unavailable or invalid.");
79
+ }
80
+ if (!actionAsset || actionAsset.visibility !== "policy" || !input.actionBytes || sha256(input.actionBytes) !== actionAsset.contentHash) {
81
+ throw new Error("Released Profile workflow actions are unavailable or invalid.");
82
+ }
83
+ const actions = ProfileWorkflowActionsSchema.parse(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.actionBytes))).actions;
84
+ const actionIds = new Set();
85
+ for (const action of actions) {
86
+ if (actionIds.has(action.id))
87
+ throw new Error(`Duplicate released Profile action ${action.id}.`);
88
+ actionIds.add(action.id);
89
+ if (!input.harnessRelease.files.some((file) => file.path.startsWith(`agents/${action.agentId}/`))) {
90
+ throw new Error(`Released Profile action ${action.id} lacks its Agent source.`);
91
+ }
92
+ }
93
+ const catalog = validateProfileWorkflowCatalog({
94
+ catalog: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.catalogBytes)),
95
+ sourcePaths: new Set(input.agentSnapshot.skills.map((skill) => skill.path)),
96
+ actionIds,
97
+ });
98
+ return { catalog, catalogHash: contentHash(catalog), actions };
99
+ }
100
+ export function resolveReleasedProfileWorkflowCatalogBinding(input) {
101
+ const binding = ProfileWorkflowBindingSchema.parse(input.binding);
102
+ if (binding.harnessRelease.id !== input.harnessRelease.id
103
+ || binding.harnessRelease.contentHash !== input.harnessRelease.contentHash) {
104
+ throw new Error("Profile workflow binding differs from the released source package.");
105
+ }
106
+ if (binding.catalogHash !== input.catalogHash)
107
+ throw new Error("Profile workflow catalog differs from its binding.");
108
+ const profile = input.harnessRelease.metadata.profile;
109
+ if (!profile || typeof profile !== "object")
110
+ throw new Error("Released source lacks Profile provenance.");
111
+ const provenance = profile;
112
+ if (provenance.id !== binding.profileId || provenance.sourceRevision !== binding.sourceRevision) {
113
+ throw new Error("Profile workflow binding differs from released Profile provenance.");
114
+ }
115
+ const workflow = input.catalog.workflows.find((candidate) => candidate.id === binding.workflowId);
116
+ if (!workflow)
117
+ throw new Error(`Profile workflow ${binding.workflowId} is absent from its bound catalog.`);
118
+ return workflow;
119
+ }
120
+ export function resolveReleasedProfileWorkflow(input) {
121
+ const binding = ProfileWorkflowBindingSchema.parse(input.binding);
122
+ const { sourcePackage, catalog, catalogHash } = loadReleasedProfileWorkflowCatalog(input.sourcePackage);
123
+ return resolveReleasedProfileWorkflowCatalogBinding({ binding, harnessRelease: sourcePackage.harnessRelease, catalog, catalogHash });
124
+ }
125
+ export function resolveProfileWorkflowBinding(input) {
126
+ const binding = ProfileWorkflowBindingSchema.parse(input.binding);
127
+ if (binding.harnessRelease.id !== input.harnessRelease.id
128
+ || binding.harnessRelease.contentHash !== input.harnessRelease.contentHash) {
129
+ throw new Error("Profile workflow binding differs from the admitted Harness release.");
130
+ }
131
+ const catalog = validateProfileWorkflowCatalog(input);
132
+ if (contentHash(catalog) !== binding.catalogHash)
133
+ throw new Error("Profile workflow catalog differs from its binding.");
134
+ const workflow = catalog.workflows.find((candidate) => candidate.id === binding.workflowId);
135
+ if (!workflow)
136
+ throw new Error(`Profile workflow ${binding.workflowId} is absent from its bound catalog.`);
137
+ return workflow;
138
+ }
@@ -0,0 +1,70 @@
1
+ export async function* providerRoundSequence(input) {
2
+ if (!Number.isInteger(input.maxRounds) || input.maxRounds < 1) {
3
+ throw new Error("Provider maxRounds must be a positive integer.");
4
+ }
5
+ for (let index = 0; index < input.maxRounds; index += 1) {
6
+ if (input.signal.aborted)
7
+ throw input.signal.reason ?? new Error("Provider loop interrupted.");
8
+ yield {
9
+ index,
10
+ requestId: `${input.turnId}:model:${index}`,
11
+ signal: input.signal
12
+ };
13
+ }
14
+ }
15
+ /** Owns provider/tool round sequencing, completion, exhaustion, and aborts. */
16
+ export async function runProviderRoundLoop(input) {
17
+ for await (const round of providerRoundSequence(input)) {
18
+ const decision = await input.runRound(round);
19
+ if (decision.type === "complete")
20
+ return decision.result;
21
+ }
22
+ return input.onExhausted();
23
+ }
24
+ /**
25
+ * Owns provider stream consumption and the normalized round result. The host
26
+ * supplies the provider request, usage recorder, and provider-specific delta
27
+ * shapes without duplicating the stream lifecycle.
28
+ */
29
+ export async function runProviderRound(input) {
30
+ let text = "";
31
+ let reasoningText = "";
32
+ let usage;
33
+ let continuation = null;
34
+ let finishReason;
35
+ const toolCallBatches = [];
36
+ try {
37
+ for await (const delta of input.stream) {
38
+ await input.onDelta?.(delta);
39
+ if (input.signal.aborted) {
40
+ throw input.signal.reason ?? new Error("Provider round interrupted.");
41
+ }
42
+ if (delta.text)
43
+ text += delta.text;
44
+ if (delta.reasoningText)
45
+ reasoningText += delta.reasoningText;
46
+ if (delta.usage !== undefined)
47
+ usage = delta.usage;
48
+ if (delta.continuation !== undefined)
49
+ continuation = delta.continuation;
50
+ if (delta.toolCalls)
51
+ toolCallBatches.push([...delta.toolCalls]);
52
+ if (delta.finishReason !== undefined)
53
+ finishReason = delta.finishReason;
54
+ }
55
+ const result = {
56
+ text,
57
+ reasoningText,
58
+ usage,
59
+ continuation,
60
+ toolCallBatches,
61
+ finishReason,
62
+ };
63
+ await input.onCompleted?.(result);
64
+ return result;
65
+ }
66
+ catch (error) {
67
+ await input.onFailed?.(error);
68
+ throw error;
69
+ }
70
+ }