@agentex/agent 0.0.27 → 0.0.28

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.
Files changed (168) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +58 -7
  3. package/dist/derived.d.ts.map +1 -1
  4. package/dist/derived.js +99 -2
  5. package/dist/derived.js.map +1 -1
  6. package/dist/goals/controller.d.ts.map +1 -1
  7. package/dist/goals/controller.js +21 -0
  8. package/dist/goals/controller.js.map +1 -1
  9. package/dist/goals/normalize.d.ts +1 -0
  10. package/dist/goals/normalize.d.ts.map +1 -1
  11. package/dist/goals/normalize.js +5 -0
  12. package/dist/goals/normalize.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/providers/claude/attach.d.ts.map +1 -1
  17. package/dist/providers/claude/attach.js +50 -9
  18. package/dist/providers/claude/attach.js.map +1 -1
  19. package/dist/providers/claude/index.d.ts.map +1 -1
  20. package/dist/providers/claude/index.js +8 -0
  21. package/dist/providers/claude/index.js.map +1 -1
  22. package/dist/providers/claude/session.d.ts +2 -0
  23. package/dist/providers/claude/session.d.ts.map +1 -1
  24. package/dist/providers/claude/session.js +17 -10
  25. package/dist/providers/claude/session.js.map +1 -1
  26. package/dist/providers/codex/attach.d.ts.map +1 -1
  27. package/dist/providers/codex/attach.js +40 -3
  28. package/dist/providers/codex/attach.js.map +1 -1
  29. package/dist/providers/codex/index.d.ts.map +1 -1
  30. package/dist/providers/codex/index.js +8 -0
  31. package/dist/providers/codex/index.js.map +1 -1
  32. package/dist/providers/codex/transcript.d.ts +3 -1
  33. package/dist/providers/codex/transcript.d.ts.map +1 -1
  34. package/dist/providers/codex/transcript.js +3 -1
  35. package/dist/providers/codex/transcript.js.map +1 -1
  36. package/dist/providers/cursor/discovery.d.ts +6 -0
  37. package/dist/providers/cursor/discovery.d.ts.map +1 -0
  38. package/dist/providers/cursor/discovery.js +118 -0
  39. package/dist/providers/cursor/discovery.js.map +1 -0
  40. package/dist/providers/cursor/execute.d.ts.map +1 -1
  41. package/dist/providers/cursor/execute.js +83 -45
  42. package/dist/providers/cursor/execute.js.map +1 -1
  43. package/dist/providers/cursor/index.d.ts.map +1 -1
  44. package/dist/providers/cursor/index.js +17 -4
  45. package/dist/providers/cursor/index.js.map +1 -1
  46. package/dist/providers/cursor/parse.js +1 -1
  47. package/dist/providers/cursor/parse.js.map +1 -1
  48. package/dist/providers/cursor/probe.d.ts +3 -0
  49. package/dist/providers/cursor/probe.d.ts.map +1 -0
  50. package/dist/providers/cursor/probe.js +89 -0
  51. package/dist/providers/cursor/probe.js.map +1 -0
  52. package/dist/providers/cursor/runtime.d.ts +4 -0
  53. package/dist/providers/cursor/runtime.d.ts.map +1 -0
  54. package/dist/providers/cursor/runtime.js +12 -0
  55. package/dist/providers/cursor/runtime.js.map +1 -0
  56. package/dist/providers/cursor/session.d.ts +3 -0
  57. package/dist/providers/cursor/session.d.ts.map +1 -0
  58. package/dist/providers/cursor/session.js +14 -0
  59. package/dist/providers/cursor/session.js.map +1 -0
  60. package/dist/providers/opencode/client.d.ts +14 -0
  61. package/dist/providers/opencode/client.d.ts.map +1 -0
  62. package/dist/providers/opencode/client.js +40 -0
  63. package/dist/providers/opencode/client.js.map +1 -0
  64. package/dist/providers/opencode/discovery.d.ts +4 -0
  65. package/dist/providers/opencode/discovery.d.ts.map +1 -0
  66. package/dist/providers/opencode/discovery.js +68 -0
  67. package/dist/providers/opencode/discovery.js.map +1 -0
  68. package/dist/providers/opencode/execute.d.ts.map +1 -1
  69. package/dist/providers/opencode/execute.js +219 -164
  70. package/dist/providers/opencode/execute.js.map +1 -1
  71. package/dist/providers/opencode/history.d.ts +24 -0
  72. package/dist/providers/opencode/history.d.ts.map +1 -0
  73. package/dist/providers/opencode/history.js +242 -0
  74. package/dist/providers/opencode/history.js.map +1 -0
  75. package/dist/providers/opencode/http-session.d.ts.map +1 -1
  76. package/dist/providers/opencode/http-session.js +333 -49
  77. package/dist/providers/opencode/http-session.js.map +1 -1
  78. package/dist/providers/opencode/index.d.ts.map +1 -1
  79. package/dist/providers/opencode/index.js +26 -3
  80. package/dist/providers/opencode/index.js.map +1 -1
  81. package/dist/providers/opencode/manager.d.ts +11 -0
  82. package/dist/providers/opencode/manager.d.ts.map +1 -0
  83. package/dist/providers/opencode/manager.js +240 -0
  84. package/dist/providers/opencode/manager.js.map +1 -0
  85. package/dist/providers/opencode/modes.d.ts +3 -0
  86. package/dist/providers/opencode/modes.d.ts.map +1 -0
  87. package/dist/providers/opencode/modes.js +30 -0
  88. package/dist/providers/opencode/modes.js.map +1 -0
  89. package/dist/providers/opencode/parse.d.ts.map +1 -1
  90. package/dist/providers/opencode/parse.js +2 -22
  91. package/dist/providers/opencode/parse.js.map +1 -1
  92. package/dist/providers/opencode/probe.d.ts +3 -0
  93. package/dist/providers/opencode/probe.d.ts.map +1 -0
  94. package/dist/providers/opencode/probe.js +107 -0
  95. package/dist/providers/opencode/probe.js.map +1 -0
  96. package/dist/providers/opencode/runtime.d.ts +8 -0
  97. package/dist/providers/opencode/runtime.d.ts.map +1 -0
  98. package/dist/providers/opencode/runtime.js +12 -0
  99. package/dist/providers/opencode/runtime.js.map +1 -0
  100. package/dist/providers/opencode/server.d.ts +18 -3
  101. package/dist/providers/opencode/server.d.ts.map +1 -1
  102. package/dist/providers/opencode/server.js +0 -0
  103. package/dist/providers/opencode/server.js.map +1 -1
  104. package/dist/providers/opencode/skill-config.d.ts +11 -0
  105. package/dist/providers/opencode/skill-config.d.ts.map +1 -0
  106. package/dist/providers/opencode/skill-config.js +57 -0
  107. package/dist/providers/opencode/skill-config.js.map +1 -0
  108. package/dist/sessions/exec-backed.d.ts +10 -0
  109. package/dist/sessions/exec-backed.d.ts.map +1 -0
  110. package/dist/sessions/exec-backed.js +141 -0
  111. package/dist/sessions/exec-backed.js.map +1 -0
  112. package/dist/sessions/history.d.ts +4 -0
  113. package/dist/sessions/history.d.ts.map +1 -0
  114. package/dist/sessions/history.js +59 -0
  115. package/dist/sessions/history.js.map +1 -0
  116. package/dist/sessions/index.d.ts +3 -0
  117. package/dist/sessions/index.d.ts.map +1 -1
  118. package/dist/sessions/index.js +2 -0
  119. package/dist/sessions/index.js.map +1 -1
  120. package/dist/types.d.ts +147 -5
  121. package/dist/types.d.ts.map +1 -1
  122. package/dist/types.js.map +1 -1
  123. package/dist/utils/auth.d.ts.map +1 -1
  124. package/dist/utils/auth.js +36 -6
  125. package/dist/utils/auth.js.map +1 -1
  126. package/dist/utils/binary.d.ts.map +1 -1
  127. package/dist/utils/binary.js +3 -0
  128. package/dist/utils/binary.js.map +1 -1
  129. package/dist/utils/endpoint.js +3 -3
  130. package/dist/utils/endpoint.js.map +1 -1
  131. package/package.json +4 -2
  132. package/src/derived.ts +126 -3
  133. package/src/goals/controller.ts +17 -0
  134. package/src/goals/normalize.ts +5 -0
  135. package/src/index.ts +13 -0
  136. package/src/providers/claude/attach.ts +48 -6
  137. package/src/providers/claude/index.ts +8 -0
  138. package/src/providers/claude/session.ts +15 -10
  139. package/src/providers/codex/attach.ts +48 -3
  140. package/src/providers/codex/index.ts +8 -0
  141. package/src/providers/codex/transcript.ts +5 -1
  142. package/src/providers/cursor/discovery.ts +114 -0
  143. package/src/providers/cursor/execute.ts +67 -30
  144. package/src/providers/cursor/index.ts +19 -5
  145. package/src/providers/cursor/parse.ts +1 -1
  146. package/src/providers/cursor/probe.ts +90 -0
  147. package/src/providers/cursor/runtime.ts +11 -0
  148. package/src/providers/cursor/session.ts +15 -0
  149. package/src/providers/opencode/client.ts +44 -0
  150. package/src/providers/opencode/discovery.ts +69 -0
  151. package/src/providers/opencode/execute.ts +64 -12
  152. package/src/providers/opencode/history.ts +270 -0
  153. package/src/providers/opencode/http-session.ts +319 -50
  154. package/src/providers/opencode/index.ts +26 -3
  155. package/src/providers/opencode/manager.ts +269 -0
  156. package/src/providers/opencode/modes.ts +31 -0
  157. package/src/providers/opencode/parse.ts +2 -22
  158. package/src/providers/opencode/probe.ts +113 -0
  159. package/src/providers/opencode/runtime.ts +13 -0
  160. package/src/providers/opencode/server.ts +0 -0
  161. package/src/providers/opencode/skill-config.ts +64 -0
  162. package/src/sessions/exec-backed.ts +169 -0
  163. package/src/sessions/history.ts +76 -0
  164. package/src/sessions/index.ts +3 -0
  165. package/src/types.ts +158 -3
  166. package/src/utils/auth.ts +35 -6
  167. package/src/utils/binary.ts +3 -0
  168. package/src/utils/endpoint.ts +3 -3
@@ -0,0 +1,169 @@
1
+ import type {
2
+ AgentSession,
3
+ CancelResult,
4
+ ClearGoalResult,
5
+ ExecutionContext,
6
+ ExecutionResult,
7
+ GoalOptions,
8
+ GoalState,
9
+ ProviderCapabilities,
10
+ SendHandle,
11
+ SendOptions,
12
+ SessionCodec,
13
+ SessionContext,
14
+ SessionState,
15
+ SetGoalResult,
16
+ StopTaskResult,
17
+ StreamEvent,
18
+ TurnResult,
19
+ } from "../types.js";
20
+ import { GoalController, EMULATED_GOAL_CAPABILITY } from "../goals/index.js";
21
+ import { uuidv7 } from "../utils/uuid.js";
22
+
23
+ export interface ExecBackedSessionOptions {
24
+ providerType: string;
25
+ execute(ctx: ExecutionContext): Promise<ExecutionResult>;
26
+ sessionCodec: SessionCodec;
27
+ ctx: SessionContext;
28
+ capabilities?: Pick<ProviderCapabilities, "goals">;
29
+ }
30
+
31
+ function turnResult(result: ExecutionResult): TurnResult {
32
+ return {
33
+ summary: result.summary,
34
+ ...(result.usage ? { usage: result.usage } : {}),
35
+ costUsd: result.costUsd,
36
+ status: result.status === "blocked" ? "failed" : result.status,
37
+ errorCode: result.errorCode,
38
+ errorMessage: result.errorMessage,
39
+ };
40
+ }
41
+
42
+ export function createExecBackedSession(options: ExecBackedSessionOptions): AgentSession {
43
+ return new ExecBackedSession(options);
44
+ }
45
+
46
+ class ExecBackedSession implements AgentSession {
47
+ private _state: SessionState = "idle";
48
+ private params: Record<string, unknown> | null;
49
+ private active: AbortController | null = null;
50
+ private inFlight: Promise<TurnResult> | null = null;
51
+ private draining = false;
52
+ private readonly goals: GoalController;
53
+
54
+ constructor(private readonly options: ExecBackedSessionOptions) {
55
+ this.params = options.sessionCodec.deserialize(options.ctx.sessionParams ?? null);
56
+ this.goals = new GoalController({
57
+ providerType: options.providerType,
58
+ capability: options.capabilities?.goals ?? EMULATED_GOAL_CAPABILITY,
59
+ getSessionId: () => this.sessionId,
60
+ send: (message) => this.send(message),
61
+ dispatch: (event) => void Promise.resolve(options.ctx.onEvent?.(event)).catch(() => undefined),
62
+ });
63
+ if (options.ctx.signal) {
64
+ if (options.ctx.signal.aborted) void this.close();
65
+ else options.ctx.signal.addEventListener("abort", () => void this.close(), { once: true });
66
+ }
67
+ }
68
+
69
+ get sessionId(): string | null {
70
+ return this.options.sessionCodec.getDisplayId?.(this.params) ?? null;
71
+ }
72
+
73
+ get state(): SessionState {
74
+ return this._state;
75
+ }
76
+
77
+ async send(message: string, sendOptions?: SendOptions): Promise<SendHandle> {
78
+ if (this._state === "closed") throw new Error("Session is closed");
79
+ if (this.draining) throw new Error("Session is draining");
80
+ if (this.inFlight) throw new Error(`${this.options.providerType} session is busy`);
81
+ const uuid = uuidv7();
82
+ const result = this.run(message, sendOptions);
83
+ this.inFlight = result;
84
+ void result.then((settled) => this.goals.onTurnSettled(settled)).catch(() => undefined);
85
+ return { uuid, result };
86
+ }
87
+
88
+ private async run(message: string, sendOptions?: SendOptions): Promise<TurnResult> {
89
+ this._state = "thinking";
90
+ const controller = new AbortController();
91
+ this.active = controller;
92
+ const onAbort = () => controller.abort();
93
+ sendOptions?.signal?.addEventListener("abort", onAbort, { once: true });
94
+ try {
95
+ const result = await this.options.execute({
96
+ prompt: message,
97
+ cwd: this.options.ctx.cwd,
98
+ env: this.options.ctx.env,
99
+ config: {
100
+ ...this.options.ctx.config,
101
+ ...(sendOptions?.timeoutSec !== undefined ? { timeoutSec: sendOptions.timeoutSec } : {}),
102
+ },
103
+ sessionParams: this.params,
104
+ signal: controller.signal,
105
+ onOutput: this.options.ctx.onOutput,
106
+ onLifecycle: (event) => this.options.ctx.onLifecycle?.(event),
107
+ onEvent: async (event: StreamEvent) => {
108
+ if (event.sessionId) {
109
+ this.params = this.options.sessionCodec.serialize({
110
+ sessionId: event.sessionId,
111
+ ...(this.options.ctx.cwd ? { cwd: this.options.ctx.cwd } : {}),
112
+ });
113
+ }
114
+ if (event.type === "tool_call") this._state = "tool_executing";
115
+ else if (event.type === "assistant" || event.type === "thinking") this._state = "thinking";
116
+ await this.options.ctx.onEvent?.(event);
117
+ },
118
+ });
119
+ if (result.clearSession) this.params = null;
120
+ if (result.sessionParams) this.params = this.options.sessionCodec.serialize(result.sessionParams);
121
+ return turnResult(result);
122
+ } finally {
123
+ sendOptions?.signal?.removeEventListener("abort", onAbort);
124
+ this.active = null;
125
+ this.inFlight = null;
126
+ this.restoreIdle();
127
+ }
128
+ }
129
+
130
+ private restoreIdle(): void {
131
+ if (this._state !== "closed") this._state = "idle";
132
+ }
133
+
134
+ async cancel(_uuid: string): Promise<CancelResult> {
135
+ return { cancelled: false };
136
+ }
137
+
138
+ async stopTask(_taskId: string): Promise<StopTaskResult> {
139
+ return { stopped: false };
140
+ }
141
+
142
+ setGoal(objective: string, goalOptions?: GoalOptions): Promise<SetGoalResult> {
143
+ return this.goals.setGoal(objective, goalOptions);
144
+ }
145
+
146
+ clearGoal(clearOptions?: { reason?: "cleared" | "blocked" }): Promise<ClearGoalResult> {
147
+ return this.goals.clearGoal(clearOptions);
148
+ }
149
+
150
+ getGoal(): GoalState | null {
151
+ return this.goals.getGoal();
152
+ }
153
+
154
+ async interrupt(): Promise<void> {
155
+ this.active?.abort();
156
+ }
157
+
158
+ async drain(): Promise<void> {
159
+ this.draining = true;
160
+ if (this.inFlight) await this.inFlight.catch(() => undefined);
161
+ await this.close();
162
+ }
163
+
164
+ async close(): Promise<void> {
165
+ this._state = "closed";
166
+ this.active?.abort();
167
+ if (this.inFlight) await this.inFlight.catch(() => undefined);
168
+ }
169
+ }
@@ -0,0 +1,76 @@
1
+ import type {
2
+ HistoryAttachment,
3
+ HistoryCatchUpOptions,
4
+ HistoryCheckpoint,
5
+ SessionAttachment,
6
+ } from "../types.js";
7
+
8
+ interface FileCheckpointValue {
9
+ offset: number;
10
+ eventId?: string;
11
+ }
12
+
13
+ function decodeCheckpoint(
14
+ checkpoint: HistoryCheckpoint | undefined,
15
+ kind: string,
16
+ ): FileCheckpointValue | undefined {
17
+ if (!checkpoint) return undefined;
18
+ if (checkpoint.kind !== kind) {
19
+ throw new Error(`history checkpoint kind must be ${kind}`);
20
+ }
21
+ const value = checkpoint.value;
22
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
23
+ throw new Error("history checkpoint value must be an object");
24
+ }
25
+ const offset = (value as Record<string, unknown>)["offset"];
26
+ const eventId = (value as Record<string, unknown>)["eventId"];
27
+ if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0) {
28
+ throw new Error("history checkpoint offset must be a non-negative safe integer");
29
+ }
30
+ if (eventId !== undefined && typeof eventId !== "string") {
31
+ throw new Error("history checkpoint eventId must be a string");
32
+ }
33
+ return { offset, ...(eventId ? { eventId } : {}) };
34
+ }
35
+
36
+ /** Adapt the legacy byte-offset transcript contract to the additive history API. */
37
+ export function historyFromSessionAttachment(
38
+ providerType: string,
39
+ attachment: SessionAttachment,
40
+ ): HistoryAttachment {
41
+ const checkpointKind = `${providerType}:byte-offset:v1`;
42
+ return {
43
+ record: attachment.record,
44
+ historySource: attachment.transcript
45
+ ? { kind: "file", path: attachment.transcript.filePath }
46
+ : null,
47
+ lastTurn: attachment.lastTurn,
48
+ catchUp(options?: HistoryCatchUpOptions) {
49
+ if (options?.mode === "bounded_full_resync") {
50
+ options = { ...options, after: undefined };
51
+ }
52
+ const decoded = decodeCheckpoint(options?.after, checkpointKind);
53
+ return {
54
+ async *[Symbol.asyncIterator]() {
55
+ for await (const yielded of attachment.catchUp({
56
+ ...(decoded ? { fromOffset: decoded.offset } : {}),
57
+ ...(decoded?.eventId ? { sinceEventId: decoded.eventId } : {}),
58
+ })) {
59
+ yield {
60
+ event: yielded.event,
61
+ eventId: yielded.eventId,
62
+ checkpoint: {
63
+ kind: checkpointKind,
64
+ value: {
65
+ offset: yielded.offset,
66
+ ...(yielded.eventId ? { eventId: yielded.eventId } : {}),
67
+ },
68
+ },
69
+ };
70
+ }
71
+ },
72
+ };
73
+ },
74
+ resume: (ctx) => attachment.resume(ctx),
75
+ };
76
+ }
@@ -6,3 +6,6 @@ export {
6
6
  assertSessionRecord,
7
7
  } from "./record.js";
8
8
  export type { CreateSessionRecordInput } from "./record.js";
9
+ export { historyFromSessionAttachment } from "./history.js";
10
+ export { createExecBackedSession } from "./exec-backed.js";
11
+ export type { ExecBackedSessionOptions } from "./exec-backed.js";
package/src/types.ts CHANGED
@@ -93,6 +93,49 @@ export interface ProviderCapabilities {
93
93
  * Codex); absent elsewhere. See internal-docs/spec-durable-sessions.md.
94
94
  */
95
95
  durableSessions?: boolean;
96
+ /** Provider-neutral durable history through `attachHistory`. */
97
+ durableHistory?: boolean;
98
+ /** A persisted provider session can be resumed. */
99
+ resume?: boolean;
100
+ /** Models expose provider-native variants independently from effort. */
101
+ modelVariants?: boolean;
102
+ /** The provider can surface interactive permission requests. */
103
+ permissionRequests?: boolean;
104
+ /** The provider can surface structured questions. */
105
+ questionRequests?: boolean;
106
+ /** Ambient MCP configuration can be excluded by a proved mechanism. */
107
+ strictMcpIsolation?: boolean;
108
+ /** Connected upstream-provider credentials can be removed safely. */
109
+ upstreamProviderDisconnect?: boolean;
110
+ /** Selection changes can be applied without starting a new host chat. */
111
+ sessionModelChange?: boolean;
112
+ sessionVariantChange?: boolean;
113
+ sessionEffortChange?: boolean;
114
+ sessionModeChange?: boolean;
115
+ }
116
+
117
+ export type CapabilityStatus = "supported" | "missing" | "upgrade_required" | "degraded";
118
+
119
+ export interface ProviderRuntimeContext {
120
+ cwd?: string;
121
+ env?: Record<string, string>;
122
+ config?: ProviderConfig;
123
+ refresh?: boolean;
124
+ }
125
+
126
+ export interface ProviderRuntimeReport {
127
+ binary: {
128
+ status: CapabilityStatus;
129
+ command: string | null;
130
+ version: string | null;
131
+ protocolProfile: string | null;
132
+ reason?: string;
133
+ };
134
+ capabilities: Partial<Record<keyof ProviderCapabilities, {
135
+ supported: boolean;
136
+ status: CapabilityStatus;
137
+ reason?: string;
138
+ }>>;
96
139
  }
97
140
 
98
141
  // ---------------------------------------------------------------------------
@@ -121,7 +164,7 @@ export interface ProviderCapabilities {
121
164
  export type GoalStatus = "active" | "paused" | "met" | "blocked" | "cleared";
122
165
 
123
166
  /** Why a `blocked` goal is blocked. */
124
- export type GoalBlockedReason = "budget" | "needs_input" | "max_iterations";
167
+ export type GoalBlockedReason = "budget" | "needs_input" | "max_iterations" | "sentinel_error";
125
168
 
126
169
  /** Who last changed the goal state. */
127
170
  export type GoalSource = "host" | "model" | "sentinel" | "agentex";
@@ -165,6 +208,8 @@ export interface GoalState {
165
208
  source: GoalSource;
166
209
  /** Why a `blocked` goal is blocked. Absent unless status === "blocked". */
167
210
  blockedReason?: GoalBlockedReason;
211
+ /** Error from goal evaluation when blockedReason is `sentinel_error`. */
212
+ errorMessage?: string;
168
213
  /** Codex telemetry, when the provider reports it (reverse-engineered fields). */
169
214
  tokensUsed?: number;
170
215
  timeUsedSeconds?: number;
@@ -286,8 +331,12 @@ export interface ProviderModule {
286
331
  */
287
332
  resolveAuth(ctx?: AuthResolveContext): Promise<AuthReport>;
288
333
  sessionCodec?: SessionCodec;
289
- /** List available models. Pass cacheTtlMs to cache results (0 = no cache, default). */
290
- listModels?(options?: { cacheTtlMs?: number }): Promise<ProviderModel[]>;
334
+ /** List available models in a runtime context. */
335
+ listModels?(options?: ListModelsOptions): Promise<ProviderModel[]>;
336
+ /** Probe the selected binary and wire protocol for effective capabilities. */
337
+ probeCapabilities?(ctx?: ProviderRuntimeContext): Promise<ProviderRuntimeReport>;
338
+ /** Optional management surface for harness-owned upstream providers. */
339
+ upstreamProviders?: UpstreamProviderManager;
291
340
  /**
292
341
  * List the operating modes this provider exposes (see `AgentMode`). Present
293
342
  * only on providers with `capabilities.modes === true`. May spawn the agent
@@ -317,6 +366,8 @@ export interface ProviderModule {
317
366
  * never auto-invoked). See internal-docs/spec-durable-sessions.md.
318
367
  */
319
368
  attachSession?(record: SessionRecord, opts?: AttachOptions): Promise<SessionAttachment>;
369
+ /** Additive file-or-service durable history surface. */
370
+ attachHistory?(record: SessionRecord, opts?: AttachOptions): Promise<HistoryAttachment>;
320
371
  }
321
372
 
322
373
  // Result of looking up a transcript for a given session.
@@ -462,6 +513,8 @@ export interface CatchUpOptions {
462
513
  export interface AttachOptions {
463
514
  /** Env overlay for home-dir resolution (same vars the transcript helpers honor). */
464
515
  env?: Record<string, string>;
516
+ /** Runtime selection for service-backed attachment. */
517
+ config?: ProviderConfig;
465
518
  }
466
519
 
467
520
  /** Read-only reattachment to a session's durable state. See `attachSession`. */
@@ -484,6 +537,36 @@ export interface SessionAttachment {
484
537
  resume(ctx?: SessionContext): Promise<AgentSession>;
485
538
  }
486
539
 
540
+ /** Opaque, provider-owned checkpoint for file or service history. */
541
+ export interface HistoryCheckpoint {
542
+ kind: string;
543
+ value: unknown;
544
+ }
545
+
546
+ export type HistorySource =
547
+ | { kind: "file"; path: string }
548
+ | { kind: "service"; description: string };
549
+
550
+ export interface HistoryCatchUpYield {
551
+ event: StreamEvent;
552
+ checkpoint: HistoryCheckpoint;
553
+ eventId: string | null;
554
+ }
555
+
556
+ export interface HistoryCatchUpOptions {
557
+ after?: HistoryCheckpoint;
558
+ mode?: "incremental" | "bounded_full_resync";
559
+ }
560
+
561
+ export interface HistoryAttachment {
562
+ record: SessionRecord;
563
+ historySource: HistorySource | null;
564
+ lastTurn: LastTurnStatus;
565
+ catchUp(opts?: HistoryCatchUpOptions): AsyncIterable<HistoryCatchUpYield>;
566
+ resume(ctx?: SessionContext): Promise<AgentSession>;
567
+ close?(): Promise<void>;
568
+ }
569
+
487
570
  // Execution input
488
571
  export interface ExecutionContext {
489
572
  prompt: string;
@@ -567,9 +650,15 @@ export interface ProviderEndpointConfig {
567
650
  export interface ProviderConfig {
568
651
  command?: string;
569
652
  model?: string;
653
+ /** Provider-native model variant, independent from reasoning effort. */
654
+ modelVariant?: string;
570
655
  /** Provider reasoning effort. Claude maps this to `--effort`; Codex sends
571
656
  * it as the app-server `turn/start.effort` override. */
572
657
  effort?: string;
658
+ /** Behavior when a host does not provide or loses its input callback. */
659
+ unattendedPermissionPolicy?: "allow" | "deny";
660
+ /** Deadline for a host to answer an observed permission or question. Default 300s. */
661
+ inputRequestTimeoutSec?: number;
573
662
  maxTurns?: number;
574
663
  /**
575
664
  * Hard runtime cap, in seconds.
@@ -1068,6 +1157,7 @@ export type StreamEvent =
1068
1157
  enforced: boolean;
1069
1158
  source: GoalSource;
1070
1159
  blockedReason?: GoalBlockedReason;
1160
+ errorMessage?: string;
1071
1161
  tokensUsed?: number;
1072
1162
  timeUsedSeconds?: number;
1073
1163
  tokenBudget?: number;
@@ -1229,6 +1319,69 @@ export interface ProviderModel {
1229
1319
  id: string;
1230
1320
  name: string;
1231
1321
  provider?: string;
1322
+ providerName?: string;
1323
+ variants?: Array<{
1324
+ id: string;
1325
+ name: string;
1326
+ description?: string;
1327
+ isDefault?: boolean;
1328
+ disabled?: boolean;
1329
+ }>;
1330
+ supportedEfforts?: string[];
1331
+ defaultEffort?: string;
1332
+ contextWindow?: number;
1333
+ maxOutputTokens?: number;
1334
+ inputCostPerMillion?: number;
1335
+ outputCostPerMillion?: number;
1336
+ supportsImages?: boolean;
1337
+ supportsTools?: boolean;
1338
+ }
1339
+
1340
+ export interface ListModelsOptions extends ProviderRuntimeContext {
1341
+ cacheTtlMs?: number;
1342
+ }
1343
+
1344
+ export interface UpstreamProvider {
1345
+ id: string;
1346
+ name: string;
1347
+ connected: boolean;
1348
+ authMethodIds: string[];
1349
+ }
1350
+
1351
+ export interface ProviderAuthMethod {
1352
+ id: string;
1353
+ name: string;
1354
+ type: "api_key" | "oauth";
1355
+ prompts?: Array<{
1356
+ id: string;
1357
+ label: string;
1358
+ type: "text" | "select";
1359
+ options?: Array<{ value: string; label: string }>;
1360
+ }>;
1361
+ }
1362
+
1363
+ export interface ProviderAuthFlow {
1364
+ id: string;
1365
+ providerId: string;
1366
+ url: string | null;
1367
+ completion: "code" | "automatic";
1368
+ instructions: string | null;
1369
+ expiresAt: string;
1370
+ }
1371
+
1372
+ export interface UpstreamProviderManager {
1373
+ list(ctx?: ProviderRuntimeContext): Promise<UpstreamProvider[]>;
1374
+ authMethods(providerId: string, ctx?: ProviderRuntimeContext): Promise<ProviderAuthMethod[]>;
1375
+ setApiKey(providerId: string, key: string, ctx?: ProviderRuntimeContext): Promise<void>;
1376
+ beginOAuth(
1377
+ providerId: string,
1378
+ methodId: string,
1379
+ inputs?: Record<string, string>,
1380
+ ctx?: ProviderRuntimeContext,
1381
+ ): Promise<ProviderAuthFlow>;
1382
+ completeOAuth(flowId: string, code?: string, ctx?: ProviderRuntimeContext): Promise<void>;
1383
+ canDisconnect(providerId: string, ctx?: ProviderRuntimeContext): Promise<boolean>;
1384
+ disconnect(providerId: string, ctx?: ProviderRuntimeContext): Promise<void>;
1232
1385
  }
1233
1386
 
1234
1387
  // MCP server configuration
@@ -1496,6 +1649,8 @@ export interface AgentSession {
1496
1649
  * `capabilities.durableSessions === true`.
1497
1650
  */
1498
1651
  describe?(): SessionRecord | null;
1652
+ /** Additive provider-neutral durable-history identity. */
1653
+ describeHistory?(): SessionRecord | null;
1499
1654
  }
1500
1655
 
1501
1656
  /** Result of a single turn within a session. */
package/src/utils/auth.ts CHANGED
@@ -89,7 +89,7 @@ export function detectAuth(providerType: string, env: Record<string, string>): R
89
89
  return { method: "subscription", billingType: "subscription" };
90
90
  }
91
91
  case "cursor": {
92
- if (hasEnv(env, "CURSOR_API_KEY") || hasEnv(env, "OPENAI_API_KEY")) {
92
+ if (hasEnv(env, "CURSOR_API_KEY")) {
93
93
  return { method: "api_key", billingType: "api" };
94
94
  }
95
95
  return { method: "subscription", billingType: "subscription" };
@@ -435,21 +435,50 @@ async function resolveGeminiAuth(ctx?: AuthResolveContext): Promise<AuthReport>
435
435
  }
436
436
 
437
437
  // ---------------------------------------------------------------------------
438
- // Cursor / OpenCode / Pi / OpenClaw / Process — no CLI auth commands;
439
- // straightforward env-only reports with binary status when applicable.
438
+ // Cursor uses its selected binary's `status` command. Other providers in this
439
+ // section use environment or native-store presence reports.
440
440
  // ---------------------------------------------------------------------------
441
441
 
442
442
  async function resolveCursorAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
443
443
  const env = buildEnv(ctx?.env);
444
444
  const binary = await checkBinary("agent", ctx?.command, env);
445
+ const apiKey = {
446
+ method: "api_key" as const,
447
+ source: { kind: "env" as const, var: "CURSOR_API_KEY" },
448
+ present: hasEnv(env, "CURSOR_API_KEY"),
449
+ };
450
+ let nativePresent = false;
451
+ let usedCli = false;
452
+ if (binary.installed && binary.resolvedPath) {
453
+ try {
454
+ const result = await runChildProcess({
455
+ runId: "cursor-auth-status",
456
+ command: binary.resolvedPath,
457
+ args: [...binary.prefixArgs, "status"],
458
+ cwd: process.cwd(),
459
+ env,
460
+ timeoutSec: 10,
461
+ });
462
+ const output = `${result.stdout}\n${result.stderr}`;
463
+ nativePresent = result.exitCode === 0 && /logged\s+in|authenticated|account\s*:/i.test(output)
464
+ && !/not\s+logged\s+in|unauthenticated/i.test(output);
465
+ usedCli = true;
466
+ } catch {
467
+ // Presence remains unknown and false for an unsupported old binary.
468
+ }
469
+ }
445
470
  return {
446
471
  providerType: "cursor",
447
472
  binary: stripPrefixArgs(binary),
448
473
  options: [
449
- { method: "api_key", source: { kind: "env", var: "CURSOR_API_KEY" }, present: hasEnv(env, "CURSOR_API_KEY") },
450
- { method: "api_key", source: { kind: "env", var: "OPENAI_API_KEY" }, present: hasEnv(env, "OPENAI_API_KEY") },
474
+ apiKey,
475
+ {
476
+ method: "subscription",
477
+ source: { kind: "cli", command: "agent status" },
478
+ present: nativePresent,
479
+ },
451
480
  ],
452
- source: "filesystem",
481
+ source: usedCli ? "cli" : "filesystem",
453
482
  };
454
483
  }
455
484
 
@@ -28,8 +28,11 @@ const COMMON_PATHS_UNIX: Record<string, string[]> = {
28
28
  ],
29
29
  agent: [
30
30
  path.join(os.homedir(), ".local", "bin", "agent"),
31
+ path.join(os.homedir(), ".local", "bin", "cursor-agent"),
31
32
  "/usr/local/bin/agent",
33
+ "/usr/local/bin/cursor-agent",
32
34
  path.join(os.homedir(), ".npm-global", "bin", "agent"),
35
+ path.join(os.homedir(), ".npm-global", "bin", "cursor-agent"),
33
36
  ],
34
37
  opencode: [
35
38
  path.join(os.homedir(), ".local", "bin", "opencode"),
@@ -158,13 +158,13 @@ function translateCodexEndpoint(e: ProviderEndpointConfig): EndpointTranslation
158
158
  // name), never argv: argv is world-readable via `ps` and a header can carry
159
159
  // a secret (Authorization, X-API-Key). Mirrors why the claude provider
160
160
  // stages MCP headers to a 0600 file instead of the command line. Header
161
- // names are assumed TOML bare keys (letters/digits/`-`/`_`), which real HTTP
162
- // header tokens are — a name with a `.` would nest as a sub-table.
161
+ // names are emitted as quoted TOML path segments because valid HTTP token
162
+ // characters such as `.` must not become nested configuration paths.
163
163
  let i = 0;
164
164
  for (const [name, value] of Object.entries(e.headers)) {
165
165
  const headerEnv = `${CODEX_CUSTOM_HEADER_ENV_PREFIX}${i++}`;
166
166
  env[headerEnv] = value;
167
- set(`model_providers.${id}.env_http_headers.${name}`, headerEnv);
167
+ set(`model_providers.${id}.env_http_headers.${JSON.stringify(name)}`, headerEnv);
168
168
  }
169
169
  }
170
170
  // modelMap intentionally ignored — Codex has no tier aliases.