@ikenga/contract 0.5.1 → 0.6.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/dist/engine/acp.d.ts +271 -0
- package/dist/engine/acp.d.ts.map +1 -0
- package/dist/engine/acp.js +13 -0
- package/dist/engine/acp.js.map +1 -0
- package/dist/{engine.d.ts → engine/adapter.d.ts} +60 -243
- package/dist/engine/adapter.d.ts.map +1 -0
- package/dist/{engine.js → engine/adapter.js} +14 -6
- package/dist/engine/adapter.js.map +1 -0
- package/dist/engine/errors.d.ts +17 -0
- package/dist/engine/errors.d.ts.map +1 -0
- package/dist/engine/errors.js +19 -0
- package/dist/engine/errors.js.map +1 -0
- package/dist/engine/index.d.ts +12 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +12 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +147 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +32 -1
- package/dist/manifest.js.map +1 -1
- package/dist/registry.d.ts +216 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +23 -0
- package/dist/registry.js.map +1 -1
- package/dist/rpc.d.ts +1 -1
- package/dist/rpc.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/{engine.ts → engine/acp.ts} +49 -198
- package/src/engine/adapter.ts +243 -0
- package/src/{engine.test.ts → engine/engine.test.ts} +33 -2
- package/src/engine/errors.ts +20 -0
- package/src/engine/index.ts +12 -0
- package/src/index.ts +1 -1
- package/src/manifest.ts +35 -1
- package/src/registry.ts +25 -0
- package/src/rpc.ts +1 -1
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikenga/contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Shared contract for the Ikenga pkg system: manifest schema, RPC types, Engine interface, capability scopes.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"import": "./dist/rpc.js"
|
|
25
25
|
},
|
|
26
26
|
"./engine": {
|
|
27
|
-
"types": "./dist/engine.d.ts",
|
|
28
|
-
"import": "./dist/engine.js"
|
|
27
|
+
"types": "./dist/engine/index.d.ts",
|
|
28
|
+
"import": "./dist/engine/index.js"
|
|
29
29
|
},
|
|
30
30
|
"./scopes": {
|
|
31
31
|
"types": "./dist/scopes.d.ts",
|
|
@@ -1,205 +1,16 @@
|
|
|
1
|
-
// Engine adapter contract. Implementations:
|
|
2
|
-
// - engine-claude-code (default, ships preinstalled)
|
|
3
|
-
// - engine-codex (future)
|
|
4
|
-
// - engine-aider (future)
|
|
5
|
-
// - engine-noop (testing / shell-without-AI mode)
|
|
6
|
-
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
|
|
9
|
-
export interface SessionOpts {
|
|
10
|
-
cwd?: string;
|
|
11
|
-
systemPrompt?: string;
|
|
12
|
-
toolAllowList?: string[];
|
|
13
|
-
/** Caller pkg id, used by the engine for per-pkg billing/audit. */
|
|
14
|
-
callerPkg?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface Session {
|
|
18
|
-
readonly id: string;
|
|
19
|
-
/** Best-effort cancellation. Resolves once the engine has stopped streaming. */
|
|
20
|
-
cancel(): Promise<void>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type EngineEvent =
|
|
24
|
-
| { type: 'message_delta'; text: string }
|
|
25
|
-
| { type: 'tool_use'; tool: string; input: unknown; toolUseId: string }
|
|
26
|
-
| { type: 'tool_result'; toolUseId: string; output: unknown; isError?: boolean }
|
|
27
|
-
| { type: 'thinking_delta'; text: string }
|
|
28
|
-
| { type: 'usage'; inputTokens: number; outputTokens: number; cacheCreationTokens?: number; cacheReadTokens?: number }
|
|
29
|
-
| { type: 'done'; reason: 'stop' | 'cancel' | 'error'; error?: string };
|
|
30
|
-
|
|
31
|
-
export interface McpServerSpec {
|
|
32
|
-
id: string;
|
|
33
|
-
command: string;
|
|
34
|
-
args?: string[];
|
|
35
|
-
env?: Record<string, string>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---------- Capabilities (single source of truth) ----------
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Capability flags every engine adapter advertises. This is the canonical
|
|
42
|
-
* shape consumed by both the shell-side ChatAdapter layer and any pkg that
|
|
43
|
-
* needs to reason about adapter features (wizard, settings UI, telemetry).
|
|
44
|
-
*
|
|
45
|
-
* Fields are intentionally a *superset* of what any single adapter
|
|
46
|
-
* supports — an adapter sets the ones it implements to `true` and the
|
|
47
|
-
* rest to `false`. Adding a new flag is a non-breaking schema change so
|
|
48
|
-
* long as adapters default it to `false` until they implement it.
|
|
49
|
-
*/
|
|
50
|
-
export const AgentCapabilitiesSchema = z.object({
|
|
51
|
-
/** Streams partial responses as they arrive (vs. all-at-once). */
|
|
52
|
-
streaming: z.boolean(),
|
|
53
|
-
/** Invokes external tools / function calls during a turn. */
|
|
54
|
-
toolUse: z.boolean(),
|
|
55
|
-
/** Emits an extended-thinking / reasoning channel separate from output. */
|
|
56
|
-
thinking: z.boolean(),
|
|
57
|
-
/** Produces structured artifacts (code blocks, files, images) the host renders. */
|
|
58
|
-
artifacts: z.boolean(),
|
|
59
|
-
/** Accepts file attachments as part of an input. */
|
|
60
|
-
fileAttachments: z.boolean(),
|
|
61
|
-
/** Accepts image input (vision). */
|
|
62
|
-
imageInput: z.boolean(),
|
|
63
|
-
/** Recognises a `/slash` command vocabulary. */
|
|
64
|
-
slashCommands: z.boolean(),
|
|
65
|
-
/** Lets the user switch models mid-thread. */
|
|
66
|
-
modelSwitching: z.boolean(),
|
|
67
|
-
/** Uses prompt-caching for repeated context (Anthropic-specific today). */
|
|
68
|
-
promptCaching: z.boolean(),
|
|
69
|
-
/** Runs agentic tools (recursive sub-agents, long-running loops). */
|
|
70
|
-
agenticTools: z.boolean(),
|
|
71
|
-
/** Speaks MCP — can register and route through MCP servers. */
|
|
72
|
-
mcp: z.boolean(),
|
|
73
|
-
/** Can resume a prior session by id. */
|
|
74
|
-
sessionResume: z.boolean(),
|
|
75
|
-
});
|
|
76
|
-
export type AgentCapabilities = z.infer<typeof AgentCapabilitiesSchema>;
|
|
77
|
-
|
|
78
|
-
// ---------- Engine pkg manifest "engine" block ----------
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Per-adapter onboarding requirements surfaced in the first-run wizard.
|
|
82
|
-
* The wizard composes these instead of hardcoding per-agent forms —
|
|
83
|
-
* every engine pkg owns its own setup story.
|
|
84
|
-
*/
|
|
85
|
-
export const EngineOnboardingSchema = z.object({
|
|
86
|
-
/** Stronghold-vault keys this adapter needs at runtime (e.g. `ANTHROPIC_API_KEY`). */
|
|
87
|
-
requiredVaultKeys: z.array(z.string()).default([]),
|
|
88
|
-
/** Plain env-vars the adapter expects on the host shell. */
|
|
89
|
-
requiredEnvVars: z.array(z.string()).default([]),
|
|
90
|
-
/**
|
|
91
|
-
* CLI command the user can run to authenticate. The wizard surfaces this
|
|
92
|
-
* as a copy-to-clipboard hint — it never shells out on the user's behalf.
|
|
93
|
-
*/
|
|
94
|
-
authCommand: z.string().optional(),
|
|
95
|
-
/** Docs URL for setting up this adapter. */
|
|
96
|
-
docsUrl: z.string().url().optional(),
|
|
97
|
-
});
|
|
98
|
-
export type EngineOnboarding = z.infer<typeof EngineOnboardingSchema>;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Manifest block declared by engine-* pkgs. Read by the shell's
|
|
102
|
-
* `AdapterLoader` at pkg-discovery time; consumed by the wizard to compose
|
|
103
|
-
* adapter-specific onboarding hints.
|
|
104
|
-
*/
|
|
105
|
-
export const EngineProvidesSchema = z.object({
|
|
106
|
-
/**
|
|
107
|
-
* Stable id — matches the detection-side agent id (e.g. `claude-code`,
|
|
108
|
-
* `codex`, `aider`, `noop`). The wizard's `selected_agent_id` is matched
|
|
109
|
-
* against this field at adapter-resolve time.
|
|
110
|
-
*/
|
|
111
|
-
agentId: z.string().min(1),
|
|
112
|
-
/** Display name; overrides any detection-side display if both present. */
|
|
113
|
-
display: z.string().optional(),
|
|
114
|
-
/** Snapshot of what this adapter implements. */
|
|
115
|
-
capabilities: AgentCapabilitiesSchema,
|
|
116
|
-
/** Onboarding requirements composed by the wizard. */
|
|
117
|
-
onboarding: EngineOnboardingSchema.default({
|
|
118
|
-
requiredVaultKeys: [],
|
|
119
|
-
requiredEnvVars: [],
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
export type EngineProvides = z.infer<typeof EngineProvidesSchema>;
|
|
123
|
-
|
|
124
|
-
// ---------- Engine adapter runtime contract ----------
|
|
125
|
-
|
|
126
|
-
export interface Engine {
|
|
127
|
-
/** Stable identifier — matches the pkg id of the engine adapter. */
|
|
128
|
-
readonly id: string;
|
|
129
|
-
|
|
130
|
-
/** Human-readable adapter version. */
|
|
131
|
-
readonly version: string;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Static metadata copied from the loading manifest's `engine` block.
|
|
135
|
-
* Required: every adapter must surface its agentId / display / capabilities
|
|
136
|
-
* / onboarding so the shell and wizard can introspect without re-parsing
|
|
137
|
-
* the manifest.
|
|
138
|
-
*/
|
|
139
|
-
readonly metadata: {
|
|
140
|
-
agentId: string;
|
|
141
|
-
display: string;
|
|
142
|
-
capabilities: AgentCapabilities;
|
|
143
|
-
onboarding: EngineOnboarding;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/** Open a new session. Sessions are cheap; create one per pkg invocation. */
|
|
147
|
-
startSession(opts: SessionOpts): Promise<Session>;
|
|
148
|
-
|
|
149
|
-
/** Send input and stream events. The iterable completes on `done`. */
|
|
150
|
-
stream(session: Session, input: string): AsyncIterable<EngineEvent>;
|
|
151
|
-
|
|
152
|
-
/** Register an MCP server with the engine. Idempotent on `id`. */
|
|
153
|
-
registerMcpServer(spec: McpServerSpec): Promise<void>;
|
|
154
|
-
|
|
155
|
-
/** Unregister an MCP server. */
|
|
156
|
-
unregisterMcpServer(id: string): Promise<void>;
|
|
157
|
-
|
|
158
|
-
/** Health check — used by the kernel before routing pkg requests. */
|
|
159
|
-
healthCheck(): Promise<{ ok: boolean; reason?: string }>;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ---------- Adapter loader contract ----------
|
|
163
|
-
|
|
164
1
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
2
|
+
* ACP (Agent Client Protocol) shapes. Phase 10: a second, ACP-shaped
|
|
3
|
+
* contract sits alongside the legacy `Engine` interface in `./adapter.ts`.
|
|
4
|
+
* The two coexist while Phase 11 retires the legacy adapter. New engines
|
|
5
|
+
* (in-process Rust ACP, Node ACP sidecars, etc.) target `AcpEngine`;
|
|
6
|
+
* existing consumers keep the `Engine` shape until they migrate.
|
|
168
7
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
8
|
+
* Method names mirror ACP's verbatim so the wire layer and the adapter
|
|
9
|
+
* layer share vocabulary — `newSession`, `prompt`, `cancel`, `setMode`,
|
|
10
|
+
* `loadSession`, `forkSession`, `requestPermission`.
|
|
172
11
|
*/
|
|
173
|
-
export interface AdapterLoader {
|
|
174
|
-
/**
|
|
175
|
-
* Load + register the engine for a given pkg manifest. The manifest must
|
|
176
|
-
* carry an `engine` block (see `EngineProvidesSchema`); the loader
|
|
177
|
-
* resolves the adapter implementation and threads the manifest's
|
|
178
|
-
* metadata into the returned `Engine.metadata`.
|
|
179
|
-
*/
|
|
180
|
-
load(manifest: { id: string; engine: EngineProvides }): Promise<Engine>;
|
|
181
|
-
|
|
182
|
-
/** Unload — used on pkg removal. After this resolves the agentId is unregistered. */
|
|
183
|
-
unload(agentId: string): Promise<void>;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Fallback returned when the requested agentId has no registered loader.
|
|
187
|
-
* Implementations MUST return a working `engine-noop` (or equivalent
|
|
188
|
-
* inert) adapter so the chat surface never dead-ends.
|
|
189
|
-
*/
|
|
190
|
-
fallback(): Engine;
|
|
191
|
-
}
|
|
192
12
|
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// Phase 10: a second, ACP-shaped contract sits alongside the legacy `Engine`
|
|
196
|
-
// interface above. The two coexist while Phase 11 retires the legacy
|
|
197
|
-
// adapter. New engines (in-process Rust ACP, Node ACP sidecars, etc.) target
|
|
198
|
-
// `AcpEngine`; existing consumers keep the `Engine` shape until they migrate.
|
|
199
|
-
//
|
|
200
|
-
// The names mirror ACP's method names verbatim so the wire layer and the
|
|
201
|
-
// adapter layer share vocabulary — `newSession`, `prompt`, `cancel`,
|
|
202
|
-
// `setMode`, `loadSession`, `forkSession`, `requestPermission`.
|
|
13
|
+
import type { McpServerSpec } from './adapter.js';
|
|
203
14
|
|
|
204
15
|
/** ACP `ProtocolVersion`. Numeric. V1 = 1. */
|
|
205
16
|
export type AcpProtocolVersion = number;
|
|
@@ -483,3 +294,43 @@ export interface AcpEngine {
|
|
|
483
294
|
/** Subscribe to OS-attention notifications. Returns a sync unsubscribe. */
|
|
484
295
|
onNotify(callback: (payload: AcpNotifyPayload) => void): () => void;
|
|
485
296
|
}
|
|
297
|
+
|
|
298
|
+
// ── Host bridge (ACP-shaped) ──────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/** Synchronous unsubscribe handle. */
|
|
301
|
+
export type AcpUnlisten = () => void;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Tauri-side surface the host shell exposes for the ACP engine. The shell
|
|
305
|
+
* binds these to its `acp_*` Tauri commands and `acp://*` event listeners.
|
|
306
|
+
*
|
|
307
|
+
* Each `on*` returns a Promise of an unsubscribe fn — the engine wraps that
|
|
308
|
+
* so callers get a sync unsubscribe.
|
|
309
|
+
*/
|
|
310
|
+
export interface AcpHost {
|
|
311
|
+
initialize(req: AcpInitializeRequest): Promise<AcpInitializeResponse>;
|
|
312
|
+
newSession(req: AcpNewSessionRequest): Promise<AcpNewSessionResponse>;
|
|
313
|
+
prompt(req: AcpPromptRequest): Promise<AcpPromptResponse>;
|
|
314
|
+
cancel(sessionId: string): Promise<void>;
|
|
315
|
+
setMode(sessionId: string, modeId: AcpSessionModeId): Promise<void>;
|
|
316
|
+
loadSession(sessionId: string): Promise<AcpLoadSessionResponse>;
|
|
317
|
+
forkSession(
|
|
318
|
+
sourceSessionId: string,
|
|
319
|
+
opts?: AcpForkOpts,
|
|
320
|
+
): Promise<AcpForkResult>;
|
|
321
|
+
listenSession(
|
|
322
|
+
sessionId: string,
|
|
323
|
+
onUpdate: (notification: AcpSessionNotification) => void,
|
|
324
|
+
): Promise<AcpUnlisten>;
|
|
325
|
+
listenPermissionRequests(
|
|
326
|
+
sessionId: string,
|
|
327
|
+
onRequest: (envelope: AcpPermissionRequestEnvelope) => void,
|
|
328
|
+
): Promise<AcpUnlisten>;
|
|
329
|
+
respondPermission(
|
|
330
|
+
requestId: string,
|
|
331
|
+
response: AcpRequestPermissionResponse,
|
|
332
|
+
): Promise<void>;
|
|
333
|
+
listenNotify(
|
|
334
|
+
callback: (payload: AcpNotifyPayload) => void,
|
|
335
|
+
): Promise<AcpUnlisten>;
|
|
336
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy Engine adapter shape. New adapters target `./acp.ts`. Both are
|
|
3
|
+
* exported through `@ikenga/contract/engine` during the deprecation cycle.
|
|
4
|
+
*
|
|
5
|
+
* Engine adapter contract. Implementations:
|
|
6
|
+
* - engine-claude-code (default, ships preinstalled)
|
|
7
|
+
* - engine-codex (future)
|
|
8
|
+
* - engine-aider (future)
|
|
9
|
+
* - engine-noop (testing / shell-without-AI mode)
|
|
10
|
+
*
|
|
11
|
+
* @deprecated Phase 11 retires this surface. New adapters target the ACP
|
|
12
|
+
* shape in `./acp.ts` (`AcpEngine`, `createAcpEngine`, `AcpHost`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
/** @deprecated Use the ACP shape in `./acp.ts`. */
|
|
18
|
+
export interface SessionOpts {
|
|
19
|
+
cwd?: string;
|
|
20
|
+
systemPrompt?: string;
|
|
21
|
+
toolAllowList?: string[];
|
|
22
|
+
/** Caller pkg id, used by the engine for per-pkg billing/audit. */
|
|
23
|
+
callerPkg?: string;
|
|
24
|
+
/** Model override for the session (adapter-specific). */
|
|
25
|
+
model?: string;
|
|
26
|
+
/** Resume a prior session by id (adapter-specific; requires sessionResume cap). */
|
|
27
|
+
resumeSessionId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @deprecated Use the ACP shape in `./acp.ts`. */
|
|
31
|
+
export interface Session {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
/** Best-effort cancellation. Resolves once the engine has stopped streaming. */
|
|
34
|
+
cancel(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @deprecated Use `AcpSessionUpdate` from `./acp.ts`. */
|
|
38
|
+
export type EngineEvent =
|
|
39
|
+
| { type: 'message_delta'; text: string }
|
|
40
|
+
| { type: 'tool_use'; tool: string; input: unknown; toolUseId: string }
|
|
41
|
+
| { type: 'tool_result'; toolUseId: string; output: unknown; isError?: boolean }
|
|
42
|
+
| { type: 'thinking_delta'; text: string }
|
|
43
|
+
| { type: 'usage'; inputTokens: number; outputTokens: number; cacheCreationTokens?: number; cacheReadTokens?: number }
|
|
44
|
+
| { type: 'done'; reason: 'stop' | 'cancel' | 'error'; error?: string };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* MCP server spec. Shared between the legacy `Engine` surface and the ACP
|
|
48
|
+
* `newSession` request — not marked `@deprecated` because the ACP shape
|
|
49
|
+
* reuses it verbatim.
|
|
50
|
+
*/
|
|
51
|
+
export interface McpServerSpec {
|
|
52
|
+
id: string;
|
|
53
|
+
command: string;
|
|
54
|
+
args?: string[];
|
|
55
|
+
env?: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------- Capabilities (single source of truth) ----------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Capability flags every engine adapter advertises. This is the canonical
|
|
62
|
+
* shape consumed by both the shell-side ChatAdapter layer and any pkg that
|
|
63
|
+
* needs to reason about adapter features (wizard, settings UI, telemetry).
|
|
64
|
+
*
|
|
65
|
+
* Fields are intentionally a *superset* of what any single adapter
|
|
66
|
+
* supports — an adapter sets the ones it implements to `true` and the
|
|
67
|
+
* rest to `false`. Adding a new flag is a non-breaking schema change so
|
|
68
|
+
* long as adapters default it to `false` until they implement it.
|
|
69
|
+
*/
|
|
70
|
+
export const AgentCapabilitiesSchema = z.object({
|
|
71
|
+
/** Streams partial responses as they arrive (vs. all-at-once). */
|
|
72
|
+
streaming: z.boolean(),
|
|
73
|
+
/** Invokes external tools / function calls during a turn. */
|
|
74
|
+
toolUse: z.boolean(),
|
|
75
|
+
/** Emits an extended-thinking / reasoning channel separate from output. */
|
|
76
|
+
thinking: z.boolean(),
|
|
77
|
+
/** Produces structured artifacts (code blocks, files, images) the host renders. */
|
|
78
|
+
artifacts: z.boolean(),
|
|
79
|
+
/** Accepts file attachments as part of an input. */
|
|
80
|
+
fileAttachments: z.boolean(),
|
|
81
|
+
/** Accepts image input (vision). */
|
|
82
|
+
imageInput: z.boolean(),
|
|
83
|
+
/** Recognises a `/slash` command vocabulary. */
|
|
84
|
+
slashCommands: z.boolean(),
|
|
85
|
+
/** Lets the user switch models mid-thread. */
|
|
86
|
+
modelSwitching: z.boolean(),
|
|
87
|
+
/** Uses prompt-caching for repeated context (Anthropic-specific today). */
|
|
88
|
+
promptCaching: z.boolean(),
|
|
89
|
+
/** Runs agentic tools (recursive sub-agents, long-running loops). */
|
|
90
|
+
agenticTools: z.boolean(),
|
|
91
|
+
/** Speaks MCP — can register and route through MCP servers. */
|
|
92
|
+
mcp: z.boolean(),
|
|
93
|
+
/** Can resume a prior session by id. */
|
|
94
|
+
sessionResume: z.boolean(),
|
|
95
|
+
});
|
|
96
|
+
export type AgentCapabilities = z.infer<typeof AgentCapabilitiesSchema>;
|
|
97
|
+
|
|
98
|
+
// ---------- Engine pkg manifest "engine" block ----------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Per-adapter onboarding requirements surfaced in the first-run wizard.
|
|
102
|
+
* The wizard composes these instead of hardcoding per-agent forms —
|
|
103
|
+
* every engine pkg owns its own setup story.
|
|
104
|
+
*/
|
|
105
|
+
export const EngineOnboardingSchema = z.object({
|
|
106
|
+
/** Stronghold-vault keys this adapter needs at runtime (e.g. `ANTHROPIC_API_KEY`). */
|
|
107
|
+
requiredVaultKeys: z.array(z.string()).default([]),
|
|
108
|
+
/** Plain env-vars the adapter expects on the host shell. */
|
|
109
|
+
requiredEnvVars: z.array(z.string()).default([]),
|
|
110
|
+
/**
|
|
111
|
+
* CLI command the user can run to authenticate. The wizard surfaces this
|
|
112
|
+
* as a copy-to-clipboard hint — it never shells out on the user's behalf.
|
|
113
|
+
*/
|
|
114
|
+
authCommand: z.string().optional(),
|
|
115
|
+
/** Docs URL for setting up this adapter. */
|
|
116
|
+
docsUrl: z.string().url().optional(),
|
|
117
|
+
});
|
|
118
|
+
export type EngineOnboarding = z.infer<typeof EngineOnboardingSchema>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Manifest block declared by engine-* pkgs. Read by the shell's
|
|
122
|
+
* `AdapterLoader` at pkg-discovery time; consumed by the wizard to compose
|
|
123
|
+
* adapter-specific onboarding hints.
|
|
124
|
+
*/
|
|
125
|
+
export const EngineProvidesSchema = z.object({
|
|
126
|
+
/**
|
|
127
|
+
* Stable id — matches the detection-side agent id (e.g. `claude-code`,
|
|
128
|
+
* `codex`, `aider`, `noop`). The wizard's `selected_agent_id` is matched
|
|
129
|
+
* against this field at adapter-resolve time.
|
|
130
|
+
*/
|
|
131
|
+
agentId: z.string().min(1),
|
|
132
|
+
/** Display name; overrides any detection-side display if both present. */
|
|
133
|
+
display: z.string().optional(),
|
|
134
|
+
/** Snapshot of what this adapter implements. */
|
|
135
|
+
capabilities: AgentCapabilitiesSchema,
|
|
136
|
+
/** Onboarding requirements composed by the wizard. */
|
|
137
|
+
onboarding: EngineOnboardingSchema.default({
|
|
138
|
+
requiredVaultKeys: [],
|
|
139
|
+
requiredEnvVars: [],
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
export type EngineProvides = z.infer<typeof EngineProvidesSchema>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Static metadata every adapter surfaces via `Engine.metadata`. Mirrors the
|
|
146
|
+
* manifest's `engine` block so the shell + wizard can introspect without
|
|
147
|
+
* re-parsing the manifest.
|
|
148
|
+
*/
|
|
149
|
+
export interface EngineMetadata {
|
|
150
|
+
agentId: string;
|
|
151
|
+
display: string;
|
|
152
|
+
capabilities: AgentCapabilities;
|
|
153
|
+
onboarding: EngineOnboarding;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------- Host bridge (legacy) ----------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Tauri-command surface the host shell exposes to legacy `Engine` adapters.
|
|
160
|
+
* The shell binds these names to its `claude_chat_*` commands; adapters
|
|
161
|
+
* never call `invoke()` directly so they remain testable.
|
|
162
|
+
*
|
|
163
|
+
* @deprecated Use the ACP shape (`AcpHost` in `./acp.ts`) for new adapters.
|
|
164
|
+
*/
|
|
165
|
+
export interface HostBridge {
|
|
166
|
+
spawn(opts: {
|
|
167
|
+
sessionId: string;
|
|
168
|
+
cwd?: string;
|
|
169
|
+
systemPrompt?: string;
|
|
170
|
+
model?: string;
|
|
171
|
+
resumeSessionId?: string;
|
|
172
|
+
}): Promise<void>;
|
|
173
|
+
send(sessionId: string, message: string): Promise<void>;
|
|
174
|
+
kill(sessionId: string): Promise<void>;
|
|
175
|
+
listen(sessionId: string): AsyncIterable<EngineEvent>;
|
|
176
|
+
registerMcp(spec: McpServerSpec): Promise<void>;
|
|
177
|
+
unregisterMcp(id: string): Promise<void>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------- Engine adapter runtime contract ----------
|
|
181
|
+
|
|
182
|
+
/** @deprecated Use `AcpEngine` from `./acp.ts`. Legacy shape retired in Phase 11. */
|
|
183
|
+
export interface Engine {
|
|
184
|
+
/** Stable identifier — matches the pkg id of the engine adapter. */
|
|
185
|
+
readonly id: string;
|
|
186
|
+
|
|
187
|
+
/** Human-readable adapter version. */
|
|
188
|
+
readonly version: string;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Static metadata copied from the loading manifest's `engine` block.
|
|
192
|
+
* Required: every adapter must surface its agentId / display / capabilities
|
|
193
|
+
* / onboarding so the shell and wizard can introspect without re-parsing
|
|
194
|
+
* the manifest.
|
|
195
|
+
*/
|
|
196
|
+
readonly metadata: EngineMetadata;
|
|
197
|
+
|
|
198
|
+
/** Open a new session. Sessions are cheap; create one per pkg invocation. */
|
|
199
|
+
startSession(opts: SessionOpts): Promise<Session>;
|
|
200
|
+
|
|
201
|
+
/** Send input and stream events. The iterable completes on `done`. */
|
|
202
|
+
stream(session: Session, input: string): AsyncIterable<EngineEvent>;
|
|
203
|
+
|
|
204
|
+
/** Register an MCP server with the engine. Idempotent on `id`. */
|
|
205
|
+
registerMcpServer(spec: McpServerSpec): Promise<void>;
|
|
206
|
+
|
|
207
|
+
/** Unregister an MCP server. */
|
|
208
|
+
unregisterMcpServer(id: string): Promise<void>;
|
|
209
|
+
|
|
210
|
+
/** Health check — used by the kernel before routing pkg requests. */
|
|
211
|
+
healthCheck(): Promise<{ ok: boolean; reason?: string }>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------- Adapter loader contract ----------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Loader the shell uses to bring engine-* pkgs online at boot, tear them
|
|
218
|
+
* down on removal, and gracefully fall back when the user's
|
|
219
|
+
* `selected_agent_id` has no installed adapter.
|
|
220
|
+
*
|
|
221
|
+
* Implementation lives shell-side (post-Phase-7); this interface lets
|
|
222
|
+
* the contract pin the shape so engine pkgs and the shell agree on the
|
|
223
|
+
* load lifecycle.
|
|
224
|
+
*/
|
|
225
|
+
export interface AdapterLoader {
|
|
226
|
+
/**
|
|
227
|
+
* Load + register the engine for a given pkg manifest. The manifest must
|
|
228
|
+
* carry an `engine` block (see `EngineProvidesSchema`); the loader
|
|
229
|
+
* resolves the adapter implementation and threads the manifest's
|
|
230
|
+
* metadata into the returned `Engine.metadata`.
|
|
231
|
+
*/
|
|
232
|
+
load(manifest: { id: string; engine: EngineProvides }): Promise<Engine>;
|
|
233
|
+
|
|
234
|
+
/** Unload — used on pkg removal. After this resolves the agentId is unregistered. */
|
|
235
|
+
unload(agentId: string): Promise<void>;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Fallback returned when the requested agentId has no registered loader.
|
|
239
|
+
* Implementations MUST return a working `engine-noop` (or equivalent
|
|
240
|
+
* inert) adapter so the chat surface never dead-ends.
|
|
241
|
+
*/
|
|
242
|
+
fallback(): Engine;
|
|
243
|
+
}
|
|
@@ -2,13 +2,14 @@ import { test } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
AgentCapabilitiesSchema,
|
|
5
|
+
EngineNotImplementedError,
|
|
5
6
|
EngineOnboardingSchema,
|
|
6
7
|
EngineProvidesSchema,
|
|
7
8
|
type AgentCapabilities,
|
|
8
9
|
type Engine,
|
|
9
10
|
type EngineProvides,
|
|
10
|
-
} from './
|
|
11
|
-
import { ManifestSchema, PkgManifestSchema } from '
|
|
11
|
+
} from './index.js';
|
|
12
|
+
import { ManifestSchema, PkgManifestSchema } from '../manifest.js';
|
|
12
13
|
|
|
13
14
|
const fullCaps: AgentCapabilities = {
|
|
14
15
|
streaming: true,
|
|
@@ -203,3 +204,33 @@ test('AdapterLoader.load consumes a manifest carrying EngineProvides', () => {
|
|
|
203
204
|
};
|
|
204
205
|
assert.equal(input.engine.agentId, 'codex');
|
|
205
206
|
});
|
|
207
|
+
|
|
208
|
+
// ---------------- EngineNotImplementedError ----------------
|
|
209
|
+
|
|
210
|
+
test('EngineNotImplementedError constructs cleanly with engineId only', () => {
|
|
211
|
+
const err = new EngineNotImplementedError('codex');
|
|
212
|
+
assert.equal(err.engineId, 'codex');
|
|
213
|
+
assert.equal(err.docsUrl, undefined);
|
|
214
|
+
assert.equal(err.message, 'Engine codex is not implemented yet');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('EngineNotImplementedError constructs with docsUrl + custom message', () => {
|
|
218
|
+
const err = new EngineNotImplementedError('gemini', {
|
|
219
|
+
docsUrl: 'https://example.com/gemini-setup',
|
|
220
|
+
message: 'Gemini adapter is gated behind feature flag',
|
|
221
|
+
});
|
|
222
|
+
assert.equal(err.engineId, 'gemini');
|
|
223
|
+
assert.equal(err.docsUrl, 'https://example.com/gemini-setup');
|
|
224
|
+
assert.equal(err.message, 'Gemini adapter is gated behind feature flag');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('EngineNotImplementedError is instanceof Error and EngineNotImplementedError', () => {
|
|
228
|
+
const err = new EngineNotImplementedError('aider');
|
|
229
|
+
assert.ok(err instanceof Error);
|
|
230
|
+
assert.ok(err instanceof EngineNotImplementedError);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("EngineNotImplementedError.name is exactly 'EngineNotImplementedError'", () => {
|
|
234
|
+
const err = new EngineNotImplementedError('codex');
|
|
235
|
+
assert.equal(err.name, 'EngineNotImplementedError');
|
|
236
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error types for engine adapters.
|
|
3
|
+
*
|
|
4
|
+
* Stub adapters (e.g. `engine-codex`, `engine-gemini`) detect a CLI on the
|
|
5
|
+
* host but don't yet implement `send` / `prompt`. They throw
|
|
6
|
+
* `EngineNotImplementedError` so the shell can render an actionable
|
|
7
|
+
* onboarding hint (the optional `docsUrl`) instead of a generic crash.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class EngineNotImplementedError extends Error {
|
|
11
|
+
readonly engineId: string;
|
|
12
|
+
readonly docsUrl?: string;
|
|
13
|
+
|
|
14
|
+
constructor(engineId: string, opts?: { docsUrl?: string; message?: string }) {
|
|
15
|
+
super(opts?.message ?? `Engine ${engineId} is not implemented yet`);
|
|
16
|
+
this.name = 'EngineNotImplementedError';
|
|
17
|
+
this.engineId = engineId;
|
|
18
|
+
this.docsUrl = opts?.docsUrl;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ikenga/contract/engine` — engine adapter surface.
|
|
3
|
+
*
|
|
4
|
+
* Exports BOTH the legacy `Engine` shape (`./adapter`) and the ACP-shaped
|
|
5
|
+
* surface (`./acp`) side-by-side for one deprecation cycle. Legacy types
|
|
6
|
+
* carry `@deprecated` JSDoc pointing at the ACP path. Shared error types
|
|
7
|
+
* live in `./errors`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './adapter.js';
|
|
11
|
+
export * from './acp.js';
|
|
12
|
+
export * from './errors.js';
|
package/src/index.ts
CHANGED
package/src/manifest.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// On disk: `<pkg-root>/manifest.json` (JSON, not TOML).
|
|
10
10
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import { EngineProvidesSchema } from './engine.js';
|
|
12
|
+
import { EngineProvidesSchema } from './engine/index.js';
|
|
13
13
|
|
|
14
14
|
export const IKENGA_API_VERSION = 1 as const;
|
|
15
15
|
export const IKENGA_API_MIN_SUPPORTED = 1 as const;
|
|
@@ -28,6 +28,14 @@ export const McpServerSchema = z.object({
|
|
|
28
28
|
env: z.record(z.string()).default({}),
|
|
29
29
|
/** "per-call" (default) | "long-lived" */
|
|
30
30
|
lifecycle: z.enum(['per-call', 'long-lived']).optional(),
|
|
31
|
+
/** Phase 9: glob patterns relative to pkg dir; supervisor restarts the
|
|
32
|
+
* long-lived child 250 ms after any matched file changes. Per-call entries
|
|
33
|
+
* ignore this. Empty = no watcher. */
|
|
34
|
+
restart_when_changed: z.array(z.string()).default([]),
|
|
35
|
+
/** Phase 9: auto-restart on unexpected exit. Default true (existing
|
|
36
|
+
* supervisor behavior). Set false for one-shot long-lived tools. Per-call
|
|
37
|
+
* entries ignore this. */
|
|
38
|
+
auto_restart: z.boolean().default(true),
|
|
31
39
|
});
|
|
32
40
|
export type McpServer = z.infer<typeof McpServerSchema>;
|
|
33
41
|
|
|
@@ -38,6 +46,12 @@ export const SidecarSpecSchema = z.object({
|
|
|
38
46
|
bin: z.string(),
|
|
39
47
|
/** "json" (default) | "raw" */
|
|
40
48
|
stdio: z.string().default('json'),
|
|
49
|
+
/** Phase 9: glob patterns relative to pkg dir; supervisor restarts the
|
|
50
|
+
* sidecar 250 ms after any matched file changes. Empty = no watcher. */
|
|
51
|
+
restart_when_changed: z.array(z.string()).default([]),
|
|
52
|
+
/** Phase 9: auto-restart on unexpected exit. Default true (existing
|
|
53
|
+
* supervisor behavior). Set false for one-shot tools. */
|
|
54
|
+
auto_restart: z.boolean().default(true),
|
|
41
55
|
});
|
|
42
56
|
export type SidecarSpec = z.infer<typeof SidecarSpecSchema>;
|
|
43
57
|
|
|
@@ -133,6 +147,18 @@ export const QueriesBlockSchema = z.object({
|
|
|
133
147
|
key_prefixes: z.array(z.string()).default([]),
|
|
134
148
|
});
|
|
135
149
|
|
|
150
|
+
/**
|
|
151
|
+
* UI preview screenshot. `path` is relative to the package's install_path
|
|
152
|
+
* for bundled pkgs; the shell resolves it to a webview-loadable URL on
|
|
153
|
+
* render. Registry pkgs surface absolute https:// URLs through the
|
|
154
|
+
* `screenshots` array on the registry entry (see `./registry.ts`).
|
|
155
|
+
*/
|
|
156
|
+
export const ScreenshotSchema = z.object({
|
|
157
|
+
path: z.string(),
|
|
158
|
+
caption: z.string().optional(),
|
|
159
|
+
});
|
|
160
|
+
export type Screenshot = z.infer<typeof ScreenshotSchema>;
|
|
161
|
+
|
|
136
162
|
// ---------- Manifest ----------
|
|
137
163
|
|
|
138
164
|
export const ManifestSchema = z.object({
|
|
@@ -171,6 +197,14 @@ export const ManifestSchema = z.object({
|
|
|
171
197
|
* See `@ikenga/contract/engine` for the source-of-truth schema.
|
|
172
198
|
*/
|
|
173
199
|
engine: EngineProvidesSchema.optional(),
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Optional UI preview screenshots surfaced by the package manager and the
|
|
203
|
+
* install sheet. `path` is relative to the package's install_path; the
|
|
204
|
+
* shell mints a webview-loadable URL for it on render. Packages without
|
|
205
|
+
* UI (engines, MCP-only servers) typically leave this empty.
|
|
206
|
+
*/
|
|
207
|
+
screenshots: z.array(ScreenshotSchema).default([]),
|
|
174
208
|
});
|
|
175
209
|
|
|
176
210
|
export type Manifest = z.infer<typeof ManifestSchema>;
|