@inixiative/agent-session 0.6.8 → 0.8.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
@@ -74,24 +74,37 @@ arrive as `rate_limit` events.
74
74
  ```ts
75
75
  import { createSession, TRANSPORTS } from "@inixiative/agent-session";
76
76
 
77
- const kind = "codex-app-server"; // or claude-cli, claude-agent-sdk
77
+ const kind = "codex-app-server"; // or claude-cli, claude-agent-sdk, muse-serve
78
78
  if (TRANSPORTS[kind].capabilities.interrupt === "acknowledged") { /* … */ }
79
79
  const session = createSession(kind, { cwd, model: "gpt-6-luna", env: { CODEX_HOME: profile } });
80
80
  ```
81
81
 
82
- | | claude-cli | codex-app-server | claude-agent-sdk | acp | api |
83
- |---|---|---|---|---|---|
84
- | Status | ✅ live | ✅ live | ✅ live | stub | stub |
85
- | Resume / fork | native / native | native / – | native / native | – | – |
86
- | Interrupt | acknowledged | acknowledged | acknowledged | – | – |
87
- | Push | – | steer | – | – | – |
88
- | Approvals | callback | callback | callback | – | – |
89
- | Limits stream / poll | ✅ / ✅ | ✅ / ✅ | ✅ / – | – | – |
90
- | Primed reset | fork | fork | – | – | – |
82
+ | | claude-cli | codex-app-server | muse-serve | claude-agent-sdk | acp | api |
83
+ |---|---|---|---|---|---|---|
84
+ | Status | ✅ live | ✅ live | ✅ live | ✅ live | stub | stub |
85
+ | Resume / fork | native / native | native / – | native / native | native / native | – | – |
86
+ | Interrupt | acknowledged | acknowledged | acknowledged | acknowledged | – | – |
87
+ | Push | – | steer | steer | – | – | – |
88
+ | Approvals | callback | callback | callback | callback | – | – |
89
+ | Limits stream / poll | ✅ / ✅ | ✅ / ✅ | ✅ / ✅ | ✅ / – | – | – |
90
+ | Primed reset | fork | fork | – | – | – | – |
91
91
 
92
92
  Codex runs only over `codex app-server --listen stdio://` (`CodexAppServerSession`);
93
93
  Codex CLI 0.155 has no `mcp-server` subcommand.
94
94
 
95
+ Muse Code runs over `muse serve` through Meta's `@muse-code/sdk` (`MuseSession`), pinned to
96
+ the CLI's version (1.4.14): a host serving another version is refused at start. It bills the
97
+ saved Muse login: metered keys and endpoints never reach it (nor Muse's own tool runs, which
98
+ inherit its environment: a repo command needing `OPENAI_API_KEY` will not see it under Muse),
99
+ and a resumed session must be on provider `meta`. Per-login isolation is `env: { XDG_CONFIG_HOME, XDG_DATA_HOME }` (login and
100
+ sessions). `musePermissions(mode)` maps the host modes to an explicit permission profile, because
101
+ the host default lets Muse's AI reviewer answer approvals instead of the gates. Questions arrive
102
+ through `onUserInput()`; unanswered ones are cancelled with a reason. An approval offering
103
+ no approve-once or reject choice is reported as an `error` event and left open (the turn ends
104
+ at its timeout). Work Muse starts itself
105
+ (cron firings, monitors, goals, reminder agents) is emitted too, marked
106
+ `unattributedReason: "task"` with a `task` lifecycle typed `muse-cron`, `muse-monitor`, ….
107
+
95
108
  Full matrix, notes and what is stubbed: [docs/transports-pools-routing.md](docs/transports-pools-routing.md).
96
109
 
97
110
  ### Tool gate
@@ -254,6 +267,7 @@ snapshots in the artifact but do not fabricate a completed-turn total.
254
267
  | Claude Code | ✅ shipped |
255
268
  | Codex app-server | ✅ shipped (`CodexAppServerSession`, `CodexPrimedSessions`) |
256
269
  | Claude Agent SDK | ✅ shipped (`ClaudeAgentSdkSession`; caller supplies `query`) |
270
+ | Muse Code | ✅ shipped (`MuseSession`, through `@muse-code/sdk`) |
257
271
  | ACP agents / direct API | typed stubs |
258
272
  | Gemini CLI | planned |
259
273
  | Grok CLI | planned (when its headless/stream mode matures) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/agent-session",
3
- "version": "0.6.8",
3
+ "version": "0.8.0",
4
4
  "description": "Drive coding-agent CLIs as persistent, streaming, event-captured sessions over interchangeable transports, with primed decision sessions, subscription pools, routing, limit polling and continuity.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -36,18 +36,19 @@
36
36
  ],
37
37
  "license": "MIT",
38
38
  "dependencies": {
39
- "@inixiative/json-rules": "^3.5.4"
39
+ "@inixiative/json-rules": "^3.16.0",
40
+ "@muse-code/sdk": "1.4.14"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@biomejs/biome": "2.5.2",
43
- "@inixiative/config": "^0.5.118",
44
- "@types/bun": "1.4.2",
44
+ "@inixiative/config": "^0.5.173",
45
+ "@types/bun": "1.4.4",
45
46
  "lefthook": "2.1.9",
46
47
  "typescript": "6.0.3"
47
48
  },
48
- "packageManager": "bun@1.4.2",
49
+ "packageManager": "bun@1.4.4",
49
50
  "engines": {
50
- "bun": ">=1.4.2"
51
+ "bun": ">=1.4.4"
51
52
  },
52
53
  "repository": {
53
54
  "type": "git",
@@ -59,6 +59,7 @@ import type {
59
59
  SessionSendOptions,
60
60
  SessionTokens,
61
61
  SubagentLifecycle,
62
+ TaskLifecycle,
62
63
  ToolDecision,
63
64
  ToolGate,
64
65
  ToolRequest,
@@ -385,6 +386,8 @@ export class ClaudeCodeSession implements HarnessSession {
385
386
  private _seenBoundaries = new Set<string>();
386
387
  /** Task ids whose task_started was a subagent (local_agent); later task frames carry no type. */
387
388
  private _agentTasks = new Set<string>();
389
+ /** Task ids whose task_started was another task type (a background shell, a monitor): id → type. */
390
+ private _otherTasks = new Map<string, string>();
388
391
 
389
392
  // -- Turn queue --
390
393
  private _queue: QueuedTurn[] = [];
@@ -829,6 +832,11 @@ export class ClaudeCodeSession implements HarnessSession {
829
832
  return turn.evidence.nativeOutcome === 'unknown' ? 'unacknowledged' : 'acknowledged';
830
833
  }
831
834
 
835
+ /** Stop a background task via the CLI control protocol (`stop_task`); its end comes as lifecycle. */
836
+ async stopTask(taskId: string, opts?: { timeoutMs?: number }): Promise<void> {
837
+ await this._controlRequest({ subtype: 'stop_task', task_id: taskId }, opts?.timeoutMs ?? 5_000);
838
+ }
839
+
832
840
  /** Account limits via the CLI control protocol (`get_usage`); no model turn. Requires a started session. */
833
841
  async readLimits(opts?: { timeoutMs?: number }): Promise<LimitSnapshot | undefined> {
834
842
  const response = await this._controlRequest(
@@ -1297,6 +1305,20 @@ export class ClaudeCodeSession implements HarnessSession {
1297
1305
  ? raw.parent_tool_use_id
1298
1306
  : undefined;
1299
1307
  const lifecycle = this._subagentLifecycle(raw);
1308
+ // A background task (a shell, a monitor) outlives its turn like a subagent: never an admission's.
1309
+ const task = lifecycle || parentToolUseId ? undefined : this._taskLifecycle(raw);
1310
+ if (task) {
1311
+ this._emit({
1312
+ kind: 'task',
1313
+ timestamp: Date.now(),
1314
+ task,
1315
+ raw,
1316
+ correlation: 'unknown',
1317
+ unattributedReason: 'task',
1318
+ nativeSessionId: typeof raw.session_id === 'string' ? raw.session_id : undefined,
1319
+ });
1320
+ return;
1321
+ }
1300
1322
  if (parentToolUseId || lifecycle) {
1301
1323
  const message = raw.message as Record<string, unknown> | undefined;
1302
1324
  const events: SessionEvent[] = lifecycle
@@ -1669,6 +1691,40 @@ export class ClaudeCodeSession implements HarnessSession {
1669
1691
  return claudeSubagentLifecycle(msg, phase, taskId);
1670
1692
  }
1671
1693
 
1694
+ /**
1695
+ * A Claude Code `system` task frame for a task that is not a subagent (`task_type` other than
1696
+ * `local_agent`), as lifecycle. Later phases carry no task type: the session remembers it.
1697
+ */
1698
+ private _taskLifecycle(msg: Record<string, unknown>): TaskLifecycle | undefined {
1699
+ if (msg.type !== 'system' || typeof msg.subtype !== 'string') return undefined;
1700
+ const phase = CLAUDE_TASK_PHASES[msg.subtype];
1701
+ const taskId = typeof msg.task_id === 'string' && msg.task_id ? msg.task_id : undefined;
1702
+ if (!phase || !taskId) return undefined;
1703
+ if (phase === 'started') {
1704
+ if (typeof msg.task_type !== 'string' || msg.task_type === 'local_agent') return undefined;
1705
+ this._otherTasks.set(taskId, msg.task_type);
1706
+ }
1707
+ const taskType = this._otherTasks.get(taskId);
1708
+ if (!taskType) return undefined;
1709
+ if (phase === 'ended') this._otherTasks.delete(taskId);
1710
+ const text = (value: unknown) => (typeof value === 'string' && value ? value : undefined);
1711
+ const patch =
1712
+ msg.patch && typeof msg.patch === 'object' && !Array.isArray(msg.patch)
1713
+ ? (msg.patch as Record<string, unknown>)
1714
+ : undefined;
1715
+ const status = text(msg.status) ?? text(patch?.status);
1716
+ return {
1717
+ phase,
1718
+ taskId,
1719
+ taskType,
1720
+ ...(text(msg.tool_use_id) ? { toolUseId: text(msg.tool_use_id) } : {}),
1721
+ ...(text(msg.description) ? { description: text(msg.description) } : {}),
1722
+ ...(status ? { status } : {}),
1723
+ ...(text(msg.reason) ? { reason: text(msg.reason) } : {}),
1724
+ ...(text(msg.summary) ? { summary: text(msg.summary) } : {}),
1725
+ };
1726
+ }
1727
+
1672
1728
  private _classify(msg: Record<string, unknown>, parentToolUseId?: string): SessionEvent[] {
1673
1729
  return this._classifyFrame(msg).map((event) =>
1674
1730
  parentToolUseId ? { ...event, parentToolUseId } : event,
@@ -11,7 +11,7 @@
11
11
  // captures every event for Oracle, and supports fork/interrupt.
12
12
  //
13
13
  // Implementations: ClaudeCodeSession, CodexAppServerSession,
14
- // ClaudeAgentSdkSession. How a model is reached is the session's transport
14
+ // ClaudeAgentSdkSession, MuseSession. How a model is reached is the session's transport
15
15
  // (see transport.ts); every transport implements this one contract.
16
16
  // ---------------------------------------------------------------------------
17
17
 
@@ -84,12 +84,13 @@ export class SessionTurnError extends Error {
84
84
  * session's cwd and config home: Claude found no transcript ("No conversation found with session
85
85
  * ID": cleaned up, deleted, or kept under another project directory or `CLAUDE_CONFIG_DIR`), or
86
86
  * Codex found no rollout ("no rollout found for thread id": deleted, or under another
87
- * `CODEX_HOME`). No turn ran. The caller decides what comes next, typically a fresh session seeded
87
+ * `CODEX_HOME`), or Muse answered `sessionNotFound` (deleted, under another `XDG_DATA_HOME`, or
88
+ * an ephemeral host that died with it). No turn ran. The caller decides what comes next, typically a fresh session seeded
88
89
  * from its own record of the conversation; `cwd` says where the lookup was made.
89
90
  */
90
91
  export class NativeBindingMissingError extends Error {
91
92
  constructor(
92
- readonly runtime: 'claude-code' | 'codex-app-server',
93
+ readonly runtime: 'claude-code' | 'codex-app-server' | 'muse',
93
94
  readonly binding: string,
94
95
  message: string,
95
96
  readonly cwd?: string,
@@ -122,7 +123,8 @@ export type SessionEventKind =
122
123
  | 'rate_limit'
123
124
  | 'result'
124
125
  | 'error'
125
- | 'subagent';
126
+ | 'subagent'
127
+ | 'task';
126
128
 
127
129
  /**
128
130
  * A subagent's lifecycle as the native runtime reports it (kind `subagent`). Claude Code: its
@@ -154,6 +156,26 @@ export interface SubagentLifecycle {
154
156
  };
155
157
  }
156
158
 
159
+ /**
160
+ * A background task that is not a subagent (kind `task`): Claude Code's `system` task frames for a
161
+ * background shell (`local_bash`), a monitor or a workflow. Like a subagent it can outlive the turn
162
+ * that started it, so its events belong to no admission (`unattributedReason: "task"`).
163
+ */
164
+ export interface TaskLifecycle {
165
+ readonly phase: 'started' | 'progress' | 'updated' | 'ended';
166
+ /** The native task id (the id the agent's own task tools name). */
167
+ readonly taskId: string;
168
+ /** The runtime's task type, e.g. `local_bash`. */
169
+ readonly taskType: string;
170
+ /** The tool call that started it, in the session's stream (its command is that call's input). */
171
+ readonly toolUseId?: string;
172
+ readonly description?: string;
173
+ /** The native status at the end (completed, failed, stopped, killed). */
174
+ readonly status?: string;
175
+ readonly reason?: string;
176
+ readonly summary?: string;
177
+ }
178
+
157
179
  /** Input excludes cache reads/writes when those disjoint counters are supplied.
158
180
  * Thinking is a subset of output; TTL counters are subsets of cacheWrite.
159
181
  * Missing optional counters mean the runtime did not report them.
@@ -191,7 +213,8 @@ export interface SessionEvent extends SessionIdentity {
191
213
  | 'unmatched-tool'
192
214
  | 'malformed-tool'
193
215
  | 'account-status'
194
- | 'subagent';
216
+ | 'subagent'
217
+ | 'task';
195
218
  /** Local transport observation, not a native terminal acknowledgment. */
196
219
  readonly transportOutcome?: SessionAttempt['transportOutcome'];
197
220
  readonly nativeOutcome?: NativeOutcome;
@@ -243,6 +266,8 @@ export interface SessionEvent extends SessionIdentity {
243
266
  readonly parentToolUseId?: string;
244
267
  /** A subagent's lifecycle (kind `subagent`). */
245
268
  readonly subagent?: SubagentLifecycle;
269
+ /** A background task's lifecycle (kind `task`). */
270
+ readonly task?: TaskLifecycle;
246
271
  }
247
272
 
248
273
  /** Result of a single send() — the turn's content plus all classified events. */
@@ -398,6 +423,11 @@ export interface HarnessSession {
398
423
  interruptNative?(opts?: { timeoutMs?: number }): Promise<NativeInterruptOutcome>;
399
424
  /** Poll account usage limits without a model turn. Present where the transport declares `limits.poll`. */
400
425
  readLimits?(opts?: { timeoutMs?: number }): Promise<LimitSnapshot | undefined>;
426
+ /**
427
+ * Stop one background task (a `task` lifecycle's `taskId`) through the runtime's own control
428
+ * channel. Its end arrives as the task's `ended` lifecycle. Claude Code: the `stop_task` request.
429
+ */
430
+ stopTask?(taskId: string, opts?: { timeoutMs?: number }): Promise<void>;
401
431
  /** One immutable admission snapshot, without copying unrelated session history. */
402
432
  inspectAttempt?(admissionId: string): SessionAttempt | undefined;
403
433
  readonly admissionProtocol?: 'prewrite-v1';
package/src/index.ts CHANGED
@@ -67,8 +67,28 @@ export {
67
67
  type LimitWindow,
68
68
  limitUtilization,
69
69
  mergeLimits,
70
+ museLimitSnapshot,
70
71
  } from './limits';
71
72
  export { type LimitProbeOptions, probeClaudeLimits, probeCodexLimits } from './limits-probe';
73
+ // Muse Code adapter: one `muse serve` host per session through Meta's @muse-code/sdk.
74
+ export {
75
+ MUSE_SDK_VERSION,
76
+ type MuseApprovalMode,
77
+ type MuseConnect,
78
+ type MuseHost,
79
+ type MuseInitializeResult,
80
+ type MuseLaunch,
81
+ type MusePermissionProfile,
82
+ type MusePermissions,
83
+ type MuseReasoningEffort,
84
+ MuseSession,
85
+ type MuseSessionConfig,
86
+ type MuseUserInputAnswer,
87
+ type MuseUserInputHandler,
88
+ type MuseUserInputQuestion,
89
+ type MuseUserInputRequest,
90
+ musePermissions,
91
+ } from './muse-session';
72
92
  export {
73
93
  ContinuityError,
74
94
  type ContinuityRefusal,
package/src/limits.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  // Subscription usage limits, normalized across runtimes. Account identity
2
2
  // (email, account/organization IDs) is never read into a snapshot.
3
3
 
4
- export type LimitRuntime = 'claude' | 'codex';
4
+ export type LimitRuntime = 'claude' | 'codex' | 'muse';
5
5
 
6
6
  export interface LimitWindow {
7
- /** Stable per runtime: Claude `five_hour`/`seven_day`/…, Codex `primary`/`secondary`. */
7
+ /** Stable per runtime: Claude `five_hour`/`seven_day`/…, Codex `primary`/`secondary`, Muse `window`/`weekly`. */
8
8
  readonly id: string;
9
9
  /** 0–100. */
10
10
  readonly usedPercent: number;
@@ -104,6 +104,42 @@ export function codexLimitSnapshot(
104
104
  };
105
105
  }
106
106
 
107
+ /**
108
+ * Muse `usage/read` result (`{ usage }`) or `usage/changed` params (the usage itself): the plan's
109
+ * rolling `window` (five hours) and `weekly` windows, in percent used. Meta reports the windows from
110
+ * its latest model response, so a host that has not served a turn yet reports none. `tier` is not read:
111
+ * on the wire it is a numeric id (the CLI shows the plan's name), not a label known to be account-free.
112
+ */
113
+ export function museLimitSnapshot(
114
+ value: unknown,
115
+ source: LimitSnapshot['source'],
116
+ observedAt = Date.now(),
117
+ ): LimitSnapshot | undefined {
118
+ const outer = object(value);
119
+ const usage = object(outer?.usage) ?? outer;
120
+ const rolling = object(usage?.window),
121
+ weekly = object(usage?.weekly);
122
+ const ms = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
123
+ const windows = [
124
+ ...window(
125
+ 'window',
126
+ percent(rolling?.usedPercent),
127
+ ms(rolling?.resetsAtMs),
128
+ typeof rolling?.windowDurationMins === 'number' ? rolling.windowDurationMins : undefined,
129
+ ),
130
+ ...window('weekly', percent(weekly?.usedPercent), ms(weekly?.resetsAtMs), 10_080),
131
+ ];
132
+ if (!windows.length) return undefined;
133
+ return {
134
+ runtime: 'muse',
135
+ source,
136
+ observedAt: ms(usage?.observedAtMs) ?? observedAt,
137
+ windows,
138
+ // Explicit both ways, so a later report clears a block an earlier one (or a quota stop) set.
139
+ blocked: windows.some((w) => w.usedPercent >= 100),
140
+ };
141
+ }
142
+
107
143
  /** Claude CLI stream-json `rate_limit_event.rate_limit_info`. Utilization is a 0–1 fraction. */
108
144
  export function claudeRateLimitSnapshot(
109
145
  info: unknown,