@tangle-network/agent-interface 0.13.0 → 0.15.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/README.md CHANGED
@@ -16,7 +16,13 @@ pnpm add @tangle-network/agent-interface
16
16
  ## Usage
17
17
 
18
18
  ```ts
19
- import type { BackendCapabilities, ProviderCapabilities } from "@tangle-network/agent-interface";
19
+ import type {
20
+ AgentEnvironmentProvider,
21
+ } from "@tangle-network/agent-interface/environment-provider";
22
+ import type {
23
+ BackendCapabilities,
24
+ ProviderCapabilities,
25
+ } from "@tangle-network/agent-interface";
20
26
 
21
27
  const caps: ProviderCapabilities = {
22
28
  supportsVision: true,
@@ -24,6 +30,36 @@ const caps: ProviderCapabilities = {
24
30
  supportsToolCalls: true,
25
31
  supportsComputerUse: false,
26
32
  };
33
+
34
+ const provider: AgentEnvironmentProvider = {
35
+ name: "example",
36
+ capabilities: () => ({
37
+ profile: {
38
+ namedProfiles: false,
39
+ systemPrompt: true,
40
+ instructions: true,
41
+ tools: true,
42
+ permissions: true,
43
+ mcp: true,
44
+ subagents: false,
45
+ resources: { files: true, instructions: true, tools: true },
46
+ hooks: false,
47
+ modes: false,
48
+ runtimeUpdate: false,
49
+ validation: true,
50
+ },
51
+ streaming: { live: true, replay: false, detach: false, turnIdempotency: false },
52
+ sessions: { continue: false, list: false, messages: false },
53
+ workspace: { read: true, write: true, exec: true, git: false, upload: false, download: false },
54
+ branching: { checkpoint: false, fork: false },
55
+ placement: false,
56
+ usage: true,
57
+ confidential: false,
58
+ }),
59
+ create: async () => {
60
+ throw new Error("implement provider create()");
61
+ },
62
+ };
27
63
  ```
28
64
 
29
65
  ## License
@@ -0,0 +1,190 @@
1
+ import type { AgentProfile, AgentProfileCapabilities, AgentProfileValidationResult } from "./agent-profile.js";
2
+ import type { InputPart, StreamEvent, TokenUsage } from "./index.js";
3
+ /** Portable profile reference: inline profile or provider catalog id. */
4
+ export type AgentProfileRef = AgentProfile | string;
5
+ export type AgentEnvironmentStatus = "pending" | "provisioning" | "running" | "stopped" | "failed" | "expired" | "unknown";
6
+ export type AgentSessionStatus = AgentEnvironmentStatus | "completed" | "cancelled";
7
+ export interface WorkspaceRequest {
8
+ /** Provider-specific environment/template id, for example "universal". */
9
+ environment?: string;
10
+ /** Container image or image alias when the provider supports image-backed workspaces. */
11
+ image?: string;
12
+ /** Repository to clone or mount before the agent runs. */
13
+ repoUrl?: string;
14
+ /** Git ref for {@link repoUrl}. */
15
+ gitRef?: string;
16
+ /** Initial working directory inside the environment. */
17
+ cwd?: string;
18
+ /** Opaque provider-native workspace fields. */
19
+ providerOptions?: Record<string, unknown>;
20
+ }
21
+ export interface ResourceRequest {
22
+ cpu?: number;
23
+ memoryMb?: number;
24
+ diskMb?: number;
25
+ gpu?: string;
26
+ providerOptions?: Record<string, unknown>;
27
+ }
28
+ export interface AgentEnvironmentQuery {
29
+ name?: string;
30
+ metadata?: Record<string, unknown>;
31
+ providerOptions?: Record<string, unknown>;
32
+ }
33
+ export interface AgentEnvironmentSummary {
34
+ id: string;
35
+ provider: string;
36
+ name?: string;
37
+ status?: AgentEnvironmentStatus;
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+ export interface ExecRequest {
41
+ cwd?: string;
42
+ env?: Record<string, string>;
43
+ timeoutMs?: number;
44
+ signal?: AbortSignal;
45
+ }
46
+ export interface ExecResult {
47
+ exitCode: number;
48
+ stdout: string;
49
+ stderr: string;
50
+ }
51
+ export interface CheckpointRequest {
52
+ name?: string;
53
+ metadata?: Record<string, unknown>;
54
+ }
55
+ export interface CheckpointRef {
56
+ id: string;
57
+ provider?: string;
58
+ metadata?: Record<string, unknown>;
59
+ }
60
+ export interface ForkRequest {
61
+ name?: string;
62
+ metadata?: Record<string, unknown>;
63
+ }
64
+ export interface PlacementInfo {
65
+ kind: "local" | "sandbox" | "fleet" | "provider";
66
+ sandboxId?: string;
67
+ fleetId?: string;
68
+ machineId?: string;
69
+ region?: string;
70
+ providerMetadata?: Record<string, unknown>;
71
+ }
72
+ export interface AgentTurnInput {
73
+ prompt?: string;
74
+ parts?: InputPart[];
75
+ sessionId?: string;
76
+ model?: string;
77
+ timeoutMs?: number;
78
+ executionId?: string;
79
+ lastEventId?: string;
80
+ turnId?: string;
81
+ detach?: boolean;
82
+ context?: Record<string, unknown>;
83
+ signal?: AbortSignal;
84
+ providerOptions?: Record<string, unknown>;
85
+ }
86
+ export interface AgentTurnResult {
87
+ text: string;
88
+ success: boolean;
89
+ error?: string;
90
+ sessionId?: string;
91
+ usage?: TokenUsage;
92
+ metadata?: Record<string, unknown>;
93
+ events?: AgentEnvironmentEvent[];
94
+ }
95
+ export interface AgentSessionRef {
96
+ id: string;
97
+ provider?: string;
98
+ metadata?: Record<string, unknown>;
99
+ }
100
+ export interface AgentEnvironmentEvent {
101
+ type: string;
102
+ data: Record<string, unknown>;
103
+ id?: string;
104
+ normalized?: StreamEvent;
105
+ usage?: TokenUsage;
106
+ providerEvent?: unknown;
107
+ }
108
+ export interface AgentSession {
109
+ readonly id: string;
110
+ status(): Promise<AgentSessionStatus | null>;
111
+ events(options?: {
112
+ since?: string;
113
+ signal?: AbortSignal;
114
+ }): AsyncIterable<AgentEnvironmentEvent>;
115
+ result(): Promise<AgentTurnResult>;
116
+ prompt(input: AgentTurnInput): Promise<AgentTurnResult>;
117
+ cancel(): Promise<void>;
118
+ }
119
+ export interface AgentEnvironment {
120
+ readonly id: string;
121
+ readonly provider: string;
122
+ readonly name?: string;
123
+ status(): Promise<AgentEnvironmentStatus>;
124
+ stream(input: AgentTurnInput): AsyncIterable<AgentEnvironmentEvent>;
125
+ dispatch?(input: AgentTurnInput): Promise<AgentSessionRef>;
126
+ session?(id: string): AgentSession;
127
+ read?(path: string, options?: {
128
+ sessionId?: string;
129
+ }): Promise<string>;
130
+ write?(path: string, content: string, options?: {
131
+ sessionId?: string;
132
+ }): Promise<void>;
133
+ exec?(command: string, options?: ExecRequest): Promise<ExecResult>;
134
+ checkpoint?(options?: CheckpointRequest): Promise<CheckpointRef>;
135
+ fork?(checkpoint: CheckpointRef, options?: ForkRequest): Promise<AgentEnvironment>;
136
+ placement?(): Promise<PlacementInfo>;
137
+ refresh?(): Promise<void>;
138
+ destroy?(): Promise<void>;
139
+ }
140
+ export interface AgentEnvironmentCapabilities {
141
+ profile: AgentProfileCapabilities;
142
+ streaming: {
143
+ live: boolean;
144
+ replay: boolean;
145
+ detach: boolean;
146
+ turnIdempotency: boolean;
147
+ };
148
+ sessions: {
149
+ continue: boolean;
150
+ list: boolean;
151
+ messages: boolean;
152
+ };
153
+ workspace: {
154
+ read: boolean;
155
+ write: boolean;
156
+ exec: boolean;
157
+ git: boolean;
158
+ upload: boolean;
159
+ download: boolean;
160
+ };
161
+ branching: {
162
+ checkpoint: boolean;
163
+ fork: boolean;
164
+ };
165
+ placement: boolean;
166
+ usage: boolean;
167
+ confidential: boolean;
168
+ }
169
+ export interface CreateAgentEnvironmentInput {
170
+ profile: AgentProfileRef;
171
+ /** Agent backend inside the provider, for example "opencode" or "codex". */
172
+ backend?: string;
173
+ workspace?: WorkspaceRequest;
174
+ resources?: ResourceRequest;
175
+ env?: Record<string, string>;
176
+ secrets?: string[] | Record<string, string>;
177
+ metadata?: Record<string, unknown>;
178
+ name?: string;
179
+ idempotencyKey?: string;
180
+ signal?: AbortSignal;
181
+ providerOptions?: Record<string, unknown>;
182
+ }
183
+ export interface AgentEnvironmentProvider {
184
+ readonly name: string;
185
+ capabilities(): AgentEnvironmentCapabilities | Promise<AgentEnvironmentCapabilities>;
186
+ validateProfile?(profile: AgentProfileRef): AgentProfileValidationResult | Promise<AgentProfileValidationResult>;
187
+ create(input: CreateAgentEnvironmentInput): Promise<AgentEnvironment>;
188
+ get?(id: string): Promise<AgentEnvironment | null>;
189
+ list?(query?: AgentEnvironmentQuery): Promise<AgentEnvironmentSummary[]>;
190
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -29,6 +29,20 @@ export declare function harnessSupportsModel(harness: HarnessType, modelId: stri
29
29
  /** The harness to adopt for a model whose provider is vendor-locked (`anthropic` → `claude-code`,
30
30
  * `openai` → `codex`, `moonshot` → `kimi-code`); `null` when any router-backed harness will do. */
31
31
  export declare function preferredHarnessForModel(modelId: string): HarnessType | null;
32
+ /**
33
+ * Keep `modelId` when the harness can run it; otherwise return the harness's best compatible id from
34
+ * `candidateIds` (preferred patterns in order, highest version within a pattern). When nothing in the
35
+ * candidate list fits, the original id is returned unchanged so the caller sees the incompatibility
36
+ * instead of a silent wrong substitution. `candidateIds` are canonical ("provider/model") ids — the
37
+ * caller maps its own catalog shape down to ids, keeping this layer catalog-agnostic.
38
+ */
39
+ export declare function snapModelToHarness(harness: HarnessType, modelId: string, candidateIds: readonly string[]): string;
40
+ /**
41
+ * Keep the harness when it can run `modelId`; otherwise return the model's native harness
42
+ * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to the router-backed
43
+ * `opencode` for everything else.
44
+ */
45
+ export declare function snapHarnessToModel(harness: HarnessType, modelId: string): HarnessType;
32
46
  /** The reasoning efforts a harness can express, independent of model — `none` up to its ceiling. */
33
47
  export declare function harnessReasoningEfforts(harness: HarnessType): readonly ReasoningEffort[];
34
48
  /** What the caller knows about a model's own reasoning capability (from a model catalog). */
@@ -29,10 +29,13 @@ export const reasoningLadder = [
29
29
  * Provider prefixes a harness is vendor-locked to (canonical-id prefix, e.g. `anthropic`, `openai`).
30
30
  * A harness with no entry is router-backed: it runs any model. Keyed by the BASE runner — aliases
31
31
  * (`claude`/`claudish`/`kimi`) resolve through `canonicalizeHarness` first.
32
+ *
33
+ * `nanoclaw` is deliberately absent despite the "claw" name: its runner routes every provider through
34
+ * the Tangle router (canonical model id straight to the gateway), so it is router-backed like
35
+ * `opencode` — not Anthropic-locked.
32
36
  */
33
37
  const harnessProviderLock = {
34
38
  "claude-code": ["anthropic"],
35
- nanoclaw: ["anthropic"],
36
39
  codex: ["openai"],
37
40
  "kimi-code": ["moonshot"],
38
41
  };
@@ -68,19 +71,69 @@ export function preferredHarnessForModel(modelId) {
68
71
  }
69
72
  return null;
70
73
  }
74
+ // ── Harness ↔ model snapping (catalog-aware) ─────────────────────────────────
75
+ /**
76
+ * Per-harness ranking patterns for {@link snapModelToHarness}, best first; within one pattern the
77
+ * highest version wins (numeric-aware). Only vendor-locked harnesses need an entry — a router-backed
78
+ * harness never snaps (it runs the model as-is). Keyed by the BASE runner (aliases canonicalized).
79
+ */
80
+ const harnessPreferredModelPatterns = {
81
+ "claude-code": [
82
+ /^anthropic\/claude-opus-[\d.-]+$/,
83
+ /^anthropic\/claude-sonnet-[\d.-]+$/,
84
+ /^anthropic\//,
85
+ ],
86
+ codex: [/^openai\/gpt-\d+(\.\d+)?$/, /^openai\/gpt/, /^openai\//],
87
+ "kimi-code": [/^moonshot\//],
88
+ };
89
+ const numericDesc = new Intl.Collator(undefined, {
90
+ numeric: true,
91
+ sensitivity: "base",
92
+ });
93
+ /**
94
+ * Keep `modelId` when the harness can run it; otherwise return the harness's best compatible id from
95
+ * `candidateIds` (preferred patterns in order, highest version within a pattern). When nothing in the
96
+ * candidate list fits, the original id is returned unchanged so the caller sees the incompatibility
97
+ * instead of a silent wrong substitution. `candidateIds` are canonical ("provider/model") ids — the
98
+ * caller maps its own catalog shape down to ids, keeping this layer catalog-agnostic.
99
+ */
100
+ export function snapModelToHarness(harness, modelId, candidateIds) {
101
+ if (harnessSupportsModel(harness, modelId))
102
+ return modelId;
103
+ const patterns = harnessPreferredModelPatterns[canonicalizeHarness(harness)] ?? [];
104
+ for (const pattern of patterns) {
105
+ const matches = candidateIds
106
+ .filter((id) => pattern.test(id))
107
+ .sort((a, b) => numericDesc.compare(b, a));
108
+ if (matches.length > 0)
109
+ return matches[0];
110
+ }
111
+ return candidateIds.find((id) => harnessSupportsModel(harness, id)) ?? modelId;
112
+ }
113
+ /**
114
+ * Keep the harness when it can run `modelId`; otherwise return the model's native harness
115
+ * (anthropic → claude-code, openai → codex, moonshot → kimi-code), falling back to the router-backed
116
+ * `opencode` for everything else.
117
+ */
118
+ export function snapHarnessToModel(harness, modelId) {
119
+ if (harnessSupportsModel(harness, modelId))
120
+ return harness;
121
+ return preferredHarnessForModel(modelId) ?? "opencode";
122
+ }
71
123
  // ── Reasoning-effort support ──────────────────────────────────────────────────
72
124
  /**
73
125
  * The highest reasoning effort a harness's runtime can express (its native clamp ceiling). Grounded
74
126
  * in cli-bridge: codex's `model_reasoning_effort` caps at `high` (xhigh/ultracode clamp down); kimi's
75
127
  * `--thinking` is binary, so `high` is its "on"; claude-code carries the full range; `cli-base` has
76
- * no agent and thus no thinking. Router/model-driven harnesses default to the full range.
128
+ * no agent and thus no thinking; `nanoclaw`'s runner sends no thinking flag, so it expresses only
129
+ * `none`. Router/model-driven harnesses default to the full range.
77
130
  */
78
131
  const harnessReasoningCeiling = {
79
132
  "cli-base": "none",
80
133
  codex: "high",
81
134
  "kimi-code": "high",
82
135
  "claude-code": "ultracode",
83
- nanoclaw: "ultracode",
136
+ nanoclaw: "none",
84
137
  };
85
138
  /** The reasoning efforts a harness can express, independent of model — `none` up to its ceiling. */
86
139
  export function harnessReasoningEfforts(harness) {
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * This package defines the contract between the sidecar and provider implementations.
6
6
  */
7
7
  import type { InteractionRequest, InteractionResponse } from "./interaction.js";
8
+ export type * from "./environment-provider.js";
8
9
  export type BackendCapabilities = {
9
10
  streaming: boolean;
10
11
  toolUse: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-interface",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -10,6 +10,11 @@
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./environment-provider": {
15
+ "import": "./dist/environment-provider.js",
16
+ "types": "./dist/environment-provider.d.ts",
17
+ "default": "./dist/environment-provider.js"
13
18
  }
14
19
  },
15
20
  "repository": {
@@ -31,11 +36,14 @@
31
36
  },
32
37
  "devDependencies": {
33
38
  "@types/node": "25.6.0",
34
- "typescript": "^6.0.3"
39
+ "typescript": "^6.0.3",
40
+ "vitest": "^4.1.5"
35
41
  },
36
42
  "scripts": {
37
43
  "build": "tsc -p tsconfig.json",
38
44
  "check-types": "tsc --noEmit",
39
- "clean": "rm -rf dist"
45
+ "clean": "rm -rf dist",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest"
40
48
  }
41
49
  }