Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-agents-share-computers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tangle-network/agent-interface": minor
---

Add minimal provider-neutral Agent instance contracts for optional managed Agents inside an existing execution environment, including workspace intent, public lifecycle records, profile identity, and idempotent stop acknowledgements.
28 changes: 25 additions & 3 deletions packages/agent-interface/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
# @tangle-network/agent-interface

Shared TypeScript types and zod schemas that define the contract between Tangle
Shared TypeScript types and Zod schemas that define the contract between Tangle
agents, the sidecar, and provider adapters: capabilities, agent profiles,
message parts, and harness descriptors. This is the canonical home for those
shapes; higher-level packages import from here rather than redefining them.

## Agent instances

`AgentProfile` describes behavior. `AgentInstanceSpec` describes one optional managed Agent inside an existing execution environment. The environment remains the computer and security boundary, so it may host zero, one, or many Agent instances.

```ts
import type { AgentInstanceSpec } from "@tangle-network/agent-interface/agent-instance";

const planner = {
id: "planner",
profile: {
name: "planner",
harness: "opencode",
prompt: { systemPrompt: "Plan before editing." },
},
workspace: { mode: "shared" },
} satisfies AgentInstanceSpec;
```

The portable contract owns only inline profile and harness selection, shared or isolated workspace intent, public lifecycle state, a provider-sanitized failure summary, and idempotent stop shapes. Credentials, HTTP routes, process identifiers, placement, billing, snapshots, local resource controls, grants, and fencing remain provider-private.

`shared` means ordinary same-computer file visibility. It is not automatic merge behavior or tenant isolation. `isolated` asks the provider for a private writable view and explicit inspect or commit behavior. Providers must reject unsatisfied machine requirements rather than silently replacing or migrating a live environment.

The public `AgentInstanceRecord` contains a credential-free profile identity, not the full profile or provider request. Existing session APIs can implement this contract without a new service: one instance maps to one managed session, compatible sessions may reuse a backend process, and stop maps to idempotent session deletion or process release.

## Durable runs, interactions, and context

`AgentRunControlRef` identifies a retained run without depending on a live JavaScript object and may carry the provider's admission digest so reconstruction can reject changed-input reuse.
Expand Down Expand Up @@ -66,8 +90,6 @@ Omitting `interactions` and `nativeContinuation`, or leaving the three durable b
`replace` means the provider deletes the harness's own system prompt and installs `prompt.systemPrompt`; `append` means it keeps that prompt and adds `prompt.appendSystemPrompt` to it.
A provider that can only append must declare `replace: false` and refuse a profile carrying `systemPrompt`, because quietly appending a requested replacement leaves the instructions the caller asked to delete in force.



## Install

```bash
Expand Down
9 changes: 9 additions & 0 deletions packages/agent-interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"import": "./dist/agent-profile.js",
"types": "./src/agent-profile.ts"
},
"./agent-instance": {
"import": "./dist/agent-instance.js",
"types": "./src/agent-instance.ts"
},
"./profile-snapshot": {
"import": "./dist/agent-profile-snapshot.js",
"types": "./src/agent-profile-snapshot.ts"
Expand Down Expand Up @@ -70,6 +74,11 @@
"types": "./dist/agent-profile.d.ts",
"default": "./dist/agent-profile.js"
},
"./agent-instance": {
"import": "./dist/agent-instance.js",
"types": "./dist/agent-instance.d.ts",
"default": "./dist/agent-instance.js"
},
"./profile-snapshot": {
"import": "./dist/agent-profile-snapshot.js",
"types": "./dist/agent-profile-snapshot.d.ts",
Expand Down
136 changes: 136 additions & 0 deletions packages/agent-interface/src/agent-instance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import {
agentInstanceRecordSchema,
agentInstanceSpecSchema,
agentInstanceStopAcknowledgementSchema,
type AgentInstanceRecord,
type AgentInstanceSpec,
} from "./agent-instance.js";

describe("agent instance contracts", () => {
it("accepts a profile-less Agent", () => {
expect(
agentInstanceSpecSchema.parse({
id: "default",
workspace: { mode: "shared" },
}),
).toEqual({
id: "default",
workspace: { mode: "shared" },
});
});

it("accepts an inline profile, harness override, and isolated workspace", () => {
const spec = {
id: "reviewer",
profile: {
name: "reviewer",
harness: "opencode",
prompt: { appendSystemPrompt: "Review every claim." },
},
harness: "claude-code",
workspace: { mode: "isolated" },
} satisfies AgentInstanceSpec;

expect(agentInstanceSpecSchema.parse(spec)).toMatchObject({
id: "reviewer",
profile: { name: "reviewer", harness: "opencode" },
harness: "claude-code",
workspace: { mode: "isolated" },
});
expectTypeOf(spec).toMatchTypeOf<AgentInstanceSpec>();
});

it("rejects provider and machine fields outside the portable contract", () => {
expect(() =>
agentInstanceSpecSchema.parse({
id: "planner",
machineShape: "profile-specific-vm",
}),
).toThrow();

expect(() =>
agentInstanceSpecSchema.parse({
id: "planner",
metadata: { apiKey: "secret" },
}),
).toThrow();
});

it("publishes only credential-free profile identity", () => {
const record = agentInstanceRecordSchema.parse({
kind: "agent-instance",
schemaVersion: 1,
id: "planner",
profile: {
name: "planner",
digest:
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
workspace: { mode: "shared" },
status: "ready",
createdAtMs: 10,
updatedAtMs: 11,
});

expect(record.profile).toEqual({
name: "planner",
digest:
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
});
});

it("requires failure evidence only for failed records", () => {
const base = {
kind: "agent-instance" as const,
schemaVersion: 1 as const,
id: "reviewer",
workspace: { mode: "shared" as const },
createdAtMs: 10,
updatedAtMs: 11,
};

expect(() =>
agentInstanceRecordSchema.parse({ ...base, status: "failed" }),
).toThrow(/failure reason/u);

expect(() =>
agentInstanceRecordSchema.parse({
...base,
status: "ready",
failure: { message: "not valid here" },
}),
).toThrow(/only for failed/u);

const failed = agentInstanceRecordSchema.parse({
...base,
status: "failed",
failure: { code: "HARNESS_EXITED", message: "process exited" },
});
expectTypeOf(failed).toMatchTypeOf<AgentInstanceRecord>();
});

it("rejects reversed timestamps and control characters", () => {
expect(() =>
agentInstanceRecordSchema.parse({
kind: "agent-instance",
schemaVersion: 1,
id: "planner",
name: "bad\nname",
workspace: { mode: "shared" },
status: "ready",
createdAtMs: 20,
updatedAtMs: 10,
}),
).toThrow();
});

it("parses idempotent stop outcomes", () => {
expect(
agentInstanceStopAcknowledgementSchema.parse({
agentId: "planner",
outcome: "already-stopped",
}),
).toEqual({ agentId: "planner", outcome: "already-stopped" });
});
});
Loading
Loading