auto-harness-client 0.3.0 → 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/README.md CHANGED
@@ -22,6 +22,30 @@ const session = await harness.createSession({
22
22
  console.log(session.url);
23
23
  ```
24
24
 
25
+ ## Target by provider or command name
26
+
27
+ `target` and `fallbacks` accept a `providerId`/`commandId` as before, or a human-readable
28
+ `providerName`/`commandName`. `createSession()` resolves each name to an id via
29
+ `listProviders()`/`listCommands()` before sending the request — at most one list call per catalog,
30
+ regardless of how many refs need it, and none at all when every ref is already id-based.
31
+
32
+ ```js
33
+ const session = await harness.createSession({
34
+ repositoryId: "repo-1",
35
+ prompt: "Review the latest changes",
36
+ target: { providerName: "codex" },
37
+ fallbacks: [{ commandName: "claude-print-plan" }],
38
+ timeout: 1_800,
39
+ });
40
+ ```
41
+
42
+ Provider names are normally unique — server-enforced on create/update — but that check is a
43
+ read-then-write race, not an atomic constraint, so `providerName` resolution still checks for
44
+ more than one match rather than trusting uniqueness. Command names are **not** server-enforced
45
+ unique at all. Either way, an unresolvable or ambiguous name throws `AutoHarnessError`
46
+ (`code === "UNKNOWN_PROVIDER_NAME"`, `"UNKNOWN_COMMAND_NAME"`, `"AMBIGUOUS_PROVIDER_NAME"`, or
47
+ `"AMBIGUOUS_COMMAND_NAME"`); the ambiguous-name message never includes the matched ids.
48
+
25
49
  ## Request deadlines
26
50
 
27
51
  Every request has a deadline that includes receiving and consuming the JSON response body.
@@ -45,10 +69,13 @@ while (page.nextCursor) {
45
69
 
46
70
  ## Principal session drains
47
71
 
48
- An automation principal can atomically stop admitting only its own sessions for one repository,
49
- then wait for the control plane to cancel and settle that exact scope. This is not repository or
50
- host drain. Use a stable idempotency key when retries may be ambiguous, poll the durable operation,
51
- and release the fence explicitly only after recording its terminal result.
72
+ Cancels this principal's own queued and running sessions for one repository, then fences new
73
+ admission from that same principal until the fence is explicitly released. **Not** repository
74
+ drain or host drain see
75
+ [Principal session drains](https://github.com/jonathanong/auto-harness/blob/main/docs/api.md#principal-session-drains)
76
+ for the full disambiguation and server-side guarantees. Use a stable idempotency key when retries
77
+ may be ambiguous, poll the durable operation, and release the fence explicitly only after
78
+ recording its terminal result.
52
79
 
53
80
  ```js
54
81
  const drain = await harness.startSessionDrain("repo-1", {
@@ -60,8 +87,10 @@ while (progress.status === "draining") {
60
87
  await new Promise((resolve) => setTimeout(resolve, 5_000));
61
88
  progress = await harness.getSessionDrain("repo-1", drain.operationId);
62
89
  }
63
- if (progress.status !== "succeeded") throw new Error(`Drain failed: ${progress.failureCode}`);
90
+ const failed = progress.status !== "succeeded";
91
+ if (failed) console.error(`Drain failed: ${progress.failureCode}`);
64
92
  await harness.releaseSessionDrain("repo-1", drain.operationId);
93
+ if (failed) throw new Error(`Drain failed: ${progress.failureCode}`);
65
94
  ```
66
95
 
67
96
  When create, clone, or resume loses to the fence, `AutoHarnessError` has `code === "DRAINING"`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-harness-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Dependency-free client for the Auto Harness automation API",
5
5
  "repository": {
6
6
  "type": "git",
package/src/errors.js ADDED
@@ -0,0 +1,20 @@
1
+ export class AutoHarnessError extends Error {
2
+ constructor(message, options) {
3
+ super(message);
4
+ this.name = "AutoHarnessError";
5
+ this.status = options.status;
6
+ this.code = options.code;
7
+ this.retryAfter = options.retryAfter;
8
+ this.operationId = options.operationId;
9
+ this.statusUrl = options.statusUrl;
10
+ }
11
+ }
12
+
13
+ export class AutoHarnessRequestTimeoutError extends Error {
14
+ constructor(timeoutMs) {
15
+ super(`Auto Harness request timed out after ${timeoutMs}ms`);
16
+ this.name = "AutoHarnessRequestTimeoutError";
17
+ this.code = "REQUEST_TIMEOUT";
18
+ this.timeoutMs = timeoutMs;
19
+ }
20
+ }
package/src/index.d.ts CHANGED
@@ -2,6 +2,23 @@ export type TargetRef =
2
2
  | { commandId: string; providerId?: never }
3
3
  | { providerId: string; commandId?: never };
4
4
 
5
+ /** A provider target, by id or by `name` — normally unique, checked defensively either way. */
6
+ export type ProviderRef =
7
+ | { providerId: string; providerName?: never; commandId?: never; commandName?: never }
8
+ | { providerName: string; providerId?: never; commandId?: never; commandName?: never };
9
+
10
+ /** A command target, by id or by `name` — command names are not required to be unique. */
11
+ export type CommandRef =
12
+ | { commandId: string; commandName?: never; providerId?: never; providerName?: never }
13
+ | { commandName: string; commandId?: never; providerId?: never; providerName?: never };
14
+
15
+ /**
16
+ * Input-only target shape for `createSession()`: an id (as `TargetRef`) or a name.
17
+ * `createSession()` resolves a name to its id via `listProviders()`/`listCommands()` before
18
+ * sending the request; a name throws on no match or on more than one match sharing that name.
19
+ */
20
+ export type TargetSpec = ProviderRef | CommandRef;
21
+
5
22
  /** Values accepted for a session metadata entry. */
6
23
  export type SessionMetadataValue = string | number | boolean | null;
7
24
 
@@ -17,8 +34,8 @@ export type CreatableSessionSource = "api" | "ui" | "webhook";
17
34
  export type CreateSessionInput = {
18
35
  repositoryId: string;
19
36
  prompt: string;
20
- target: TargetRef;
21
- fallbacks?: TargetRef[];
37
+ target: TargetSpec;
38
+ fallbacks?: TargetSpec[];
22
39
  ref?: string;
23
40
  concurrencyId?: string;
24
41
  queueTtlSeconds?: number;
@@ -120,9 +137,51 @@ export type RepositoryPage = {
120
137
  nextCursor: string | null;
121
138
  };
122
139
 
140
+ /** Operator-supplied per-token vendor rates; Auto Harness never fetches vendor prices. */
141
+ export type UsageRates = {
142
+ inputTokenMicros?: string;
143
+ outputTokenMicros?: string;
144
+ cachedInputTokenMicros?: string;
145
+ reasoningTokenMicros?: string;
146
+ currency: string;
147
+ };
148
+
149
+ /** Global catalog entry: an AI CLI vendor, keyed by a unique, server-enforced `name`. */
150
+ export type Provider = {
151
+ id: string;
152
+ /** e.g. "claude", "codex", "grok" */
153
+ name: string;
154
+ defaultCommandId: string | null;
155
+ createdAt: string;
156
+ updatedAt: string;
157
+ usageRates?: UsageRates;
158
+ };
159
+
160
+ /** Bounded literal-prefix policy used by the agent to extract a native resume reference. */
161
+ export type ResumeRefCapture = {
162
+ stream: "stdout" | "stderr" | "either";
163
+ linePrefix: string;
164
+ };
165
+
166
+ /** Global catalog entry: a named command invocation. `name` is not required to be unique. */
167
+ export type Command = {
168
+ id: string;
169
+ /** e.g. "claude-print", "echo hello world" */
170
+ name: string;
171
+ argv: string[];
172
+ appendPrompt: boolean;
173
+ appendPromptSeparator?: boolean;
174
+ resumeArgvTemplate?: string[];
175
+ resumeRefCapture?: ResumeRefCapture;
176
+ /** FK to Provider, or null for a standalone command that runs anywhere ungated. */
177
+ providerId: string | null;
178
+ createdAt: string;
179
+ updatedAt: string;
180
+ };
181
+
123
182
  export type SessionDrainStatus = "draining" | "succeeded" | "failed" | "released";
124
183
 
125
- /** Bounded, durable progress for the authenticated principal's repository session drain. */
184
+ /** Bounded, durable progress for a principal session drain: cancels the authenticated principal's own queued/running sessions for one repository (not repository or host drain). */
126
185
  export type SessionDrain = {
127
186
  operationId: string;
128
187
  repositoryId: string;
@@ -140,6 +199,13 @@ export type SessionDrain = {
140
199
  failureCode?: string;
141
200
  };
142
201
 
202
+ /**
203
+ * Thrown for a failed HTTP response, and also, with `status: 400`, when `createSession()`
204
+ * cannot resolve a `TargetSpec` name: `code === "UNKNOWN_PROVIDER_NAME"` /
205
+ * `"UNKNOWN_COMMAND_NAME"` for no match, `"AMBIGUOUS_PROVIDER_NAME"` /
206
+ * `"AMBIGUOUS_COMMAND_NAME"` for more than one match sharing a name — that message never
207
+ * includes the matched ids.
208
+ */
143
209
  export class AutoHarnessError extends Error {
144
210
  status: number;
145
211
  code: string;
@@ -181,6 +247,7 @@ export class AutoHarnessClient {
181
247
  cancelSession(id: string): Promise<Session>;
182
248
  resumeSession(id: string, input?: ResumeSessionInput): Promise<Session & { created: boolean }>;
183
249
  listSessions(options?: ListSessionsOptions): Promise<SessionPage>;
250
+ /** Cancels this principal's own queued/running sessions for one repository and fences new admission from it — not repository or host drain. */
184
251
  startSessionDrain(
185
252
  repositoryId: string,
186
253
  options?: { idempotencyKey?: string },
@@ -191,4 +258,6 @@ export class AutoHarnessClient {
191
258
  pauseRepository(id: string): Promise<Repository>;
192
259
  drainRepository(id: string): Promise<Repository>;
193
260
  activateRepository(id: string): Promise<Repository>;
261
+ listProviders(): Promise<Provider[]>;
262
+ listCommands(): Promise<Command[]>;
194
263
  }
package/src/index.js CHANGED
@@ -1,23 +1,7 @@
1
- export class AutoHarnessError extends Error {
2
- constructor(message, options) {
3
- super(message);
4
- this.name = "AutoHarnessError";
5
- this.status = options.status;
6
- this.code = options.code;
7
- this.retryAfter = options.retryAfter;
8
- this.operationId = options.operationId;
9
- this.statusUrl = options.statusUrl;
10
- }
11
- }
1
+ import { AutoHarnessError, AutoHarnessRequestTimeoutError } from "./errors.js";
2
+ import { resolveCreateSessionTargets } from "./resolve-target.js";
12
3
 
13
- export class AutoHarnessRequestTimeoutError extends Error {
14
- constructor(timeoutMs) {
15
- super(`Auto Harness request timed out after ${timeoutMs}ms`);
16
- this.name = "AutoHarnessRequestTimeoutError";
17
- this.code = "REQUEST_TIMEOUT";
18
- this.timeoutMs = timeoutMs;
19
- }
20
- }
4
+ export { AutoHarnessError, AutoHarnessRequestTimeoutError };
21
5
 
22
6
  export class AutoHarnessClient {
23
7
  constructor(options) {
@@ -84,8 +68,9 @@ export class AutoHarnessClient {
84
68
  }
85
69
  }
86
70
 
87
- createSession(input) {
88
- return this.request("/sessions", { method: "POST", body: JSON.stringify(input) });
71
+ async createSession(input) {
72
+ const body = await resolveCreateSessionTargets(this, input);
73
+ return this.request("/sessions", { method: "POST", body: JSON.stringify(body) });
89
74
  }
90
75
 
91
76
  getSession(id) {
@@ -170,4 +155,14 @@ export class AutoHarnessClient {
170
155
  repositoryOperation(id, operation) {
171
156
  return this.request(`/repositories/${encodeURIComponent(id)}/${operation}`, { method: "POST" });
172
157
  }
158
+
159
+ async listProviders() {
160
+ const { items } = await this.request("/providers");
161
+ return items;
162
+ }
163
+
164
+ async listCommands() {
165
+ const { items } = await this.request("/commands");
166
+ return items;
167
+ }
173
168
  }
@@ -0,0 +1,54 @@
1
+ import { AutoHarnessError } from "./errors.js";
2
+
3
+ async function resolveByName(name, catalog, kind, idKey) {
4
+ const matches = (await catalog()).filter((entry) => entry.name === name);
5
+ if (matches.length === 0) {
6
+ throw new AutoHarnessError(`no ${kind} named "${name}"`, {
7
+ status: 400,
8
+ code: `UNKNOWN_${kind.toUpperCase()}_NAME`,
9
+ });
10
+ }
11
+ if (matches.length > 1) {
12
+ throw new AutoHarnessError(
13
+ `ambiguous ${kind} name "${name}": ${matches.length} ${kind}s share this name`,
14
+ { status: 400, code: `AMBIGUOUS_${kind.toUpperCase()}_NAME` },
15
+ );
16
+ }
17
+ return { [idKey]: matches[0].id };
18
+ }
19
+
20
+ async function resolveRef(ref, providers, commands) {
21
+ if (ref == null || typeof ref !== "object") return ref;
22
+ // Checked by value, not `in`: a name ref built by conditional spreading can carry an
23
+ // explicit `providerId: undefined`, which `in` would treat as already resolved.
24
+ if (ref.providerId !== undefined || ref.commandId !== undefined) return ref;
25
+ if (ref.providerName !== undefined) {
26
+ return resolveByName(ref.providerName, providers, "provider", "providerId");
27
+ }
28
+ if (ref.commandName !== undefined) {
29
+ return resolveByName(ref.commandName, commands, "command", "commandId");
30
+ }
31
+ return ref;
32
+ }
33
+
34
+ /**
35
+ * Resolves `providerName`/`commandName` entries in `input.target`/`input.fallbacks` to
36
+ * `providerId`/`commandId` via `client.listProviders()`/`listCommands()`, called at most once
37
+ * each regardless of how many refs need them. Id-shaped refs pass through untouched, so an
38
+ * all-id call makes no extra requests. Provider and command names are both resolved defensively
39
+ * against more than one match — provider names are server-enforced unique today, but the
40
+ * create/update check is a read-then-write race, not an atomic constraint.
41
+ */
42
+ export async function resolveCreateSessionTargets(client, input) {
43
+ let providersPromise;
44
+ let commandsPromise;
45
+ const providers = () => (providersPromise ??= client.listProviders());
46
+ const commands = () => (commandsPromise ??= client.listCommands());
47
+
48
+ const target = await resolveRef(input.target, providers, commands);
49
+ if (input.fallbacks === undefined) return { ...input, target };
50
+ const fallbacks = await Promise.all(
51
+ input.fallbacks.map((fallback) => resolveRef(fallback, providers, commands)),
52
+ );
53
+ return { ...input, target, fallbacks };
54
+ }