@agi-cli/server 0.1.139 → 0.1.141

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 (34) hide show
  1. package/package.json +3 -3
  2. package/src/events/types.ts +2 -0
  3. package/src/index.ts +4 -0
  4. package/src/openapi/schemas.ts +3 -3
  5. package/src/routes/config/defaults.ts +3 -0
  6. package/src/routes/config/main.ts +5 -0
  7. package/src/routes/config/models.ts +3 -3
  8. package/src/routes/git/commit.ts +1 -1
  9. package/src/routes/git/schemas.ts +1 -1
  10. package/src/routes/session-approval.ts +53 -0
  11. package/src/routes/session-messages.ts +2 -2
  12. package/src/runtime/agent/runner-setup.ts +14 -29
  13. package/src/runtime/message/compaction-limits.ts +5 -2
  14. package/src/runtime/message/history-builder.ts +1 -1
  15. package/src/runtime/message/service.ts +7 -3
  16. package/src/runtime/provider/index.ts +4 -0
  17. package/src/runtime/provider/moonshot.ts +8 -0
  18. package/src/runtime/session/branch.ts +2 -2
  19. package/src/runtime/session/db-operations.ts +17 -25
  20. package/src/runtime/session/queue.ts +3 -1
  21. package/src/runtime/stream/finish-handler.ts +3 -3
  22. package/src/runtime/stream/step-finish.ts +4 -4
  23. package/src/runtime/stream/types.ts +1 -1
  24. package/src/runtime/tools/approval.ts +179 -0
  25. package/src/runtime/tools/context.ts +2 -0
  26. package/src/runtime/tools/setup.ts +1 -0
  27. package/src/tools/adapter.ts +28 -0
  28. package/src/tools/database/get-parent-session.ts +1 -1
  29. package/src/tools/database/get-session-context.ts +1 -1
  30. package/src/tools/database/present-session-links.ts +1 -1
  31. package/src/tools/database/query-messages.ts +1 -1
  32. package/src/tools/database/query-sessions.ts +1 -1
  33. package/src/tools/database/search-history.ts +1 -1
  34. package/sst-env.d.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/server",
3
- "version": "0.1.139",
3
+ "version": "0.1.141",
4
4
  "description": "HTTP API server for AGI CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,8 +29,8 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@agi-cli/sdk": "0.1.139",
33
- "@agi-cli/database": "0.1.139",
32
+ "@agi-cli/sdk": "0.1.141",
33
+ "@agi-cli/database": "0.1.141",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
@@ -1,4 +1,6 @@
1
1
  export type AGIEventType =
2
+ | 'tool.approval.required'
3
+ | 'tool.approval.resolved'
2
4
  | 'solforge.payment.required'
3
5
  | 'solforge.payment.signing'
4
6
  | 'solforge.payment.complete'
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { registerTerminalsRoutes } from './routes/terminals.ts';
16
16
  import { registerSessionFilesRoutes } from './routes/session-files.ts';
17
17
  import { registerBranchRoutes } from './routes/branch.ts';
18
18
  import { registerResearchRoutes } from './routes/research.ts';
19
+ import { registerSessionApprovalRoute } from './routes/session-approval.ts';
19
20
  import { registerSolforgeRoutes } from './routes/solforge.ts';
20
21
  import type { AgentConfigEntry } from './runtime/agent/registry.ts';
21
22
 
@@ -59,6 +60,7 @@ function initApp() {
59
60
  registerRootRoutes(app);
60
61
  registerOpenApiRoute(app);
61
62
  registerSessionsRoutes(app);
63
+ registerSessionApprovalRoute(app);
62
64
  registerSessionMessagesRoutes(app);
63
65
  registerSessionStreamRoute(app);
64
66
  registerAskRoutes(app);
@@ -128,6 +130,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
128
130
  registerRootRoutes(honoApp);
129
131
  registerOpenApiRoute(honoApp);
130
132
  registerSessionsRoutes(honoApp);
133
+ registerSessionApprovalRoute(honoApp);
131
134
  registerSessionMessagesRoutes(honoApp);
132
135
  registerSessionStreamRoute(honoApp);
133
136
  registerAskRoutes(honoApp);
@@ -225,6 +228,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
225
228
  registerRootRoutes(honoApp);
226
229
  registerOpenApiRoute(honoApp);
227
230
  registerSessionsRoutes(honoApp);
231
+ registerSessionApprovalRoute(honoApp);
228
232
  registerSessionMessagesRoutes(honoApp);
229
233
  registerSessionStreamRoute(honoApp);
230
234
  registerAskRoutes(honoApp);
@@ -91,8 +91,8 @@ export const schemas = {
91
91
  createdAt: { type: 'integer', format: 'int64' },
92
92
  completedAt: { type: 'integer', format: 'int64', nullable: true },
93
93
  latencyMs: { type: 'integer', nullable: true },
94
- promptTokens: { type: 'integer', nullable: true },
95
- completionTokens: { type: 'integer', nullable: true },
94
+ inputTokens: { type: 'integer', nullable: true },
95
+ outputTokens: { type: 'integer', nullable: true },
96
96
  totalTokens: { type: 'integer', nullable: true },
97
97
  error: { type: 'string', nullable: true },
98
98
  },
@@ -206,7 +206,7 @@ export const schemas = {
206
206
  id: { type: 'string' },
207
207
  label: { type: 'string' },
208
208
  toolCall: { type: 'boolean' },
209
- reasoning: { type: 'boolean' },
209
+ reasoningText: { type: 'boolean' },
210
210
  },
211
211
  required: ['id', 'label'],
212
212
  },
@@ -11,6 +11,7 @@ export function registerDefaultsRoute(app: Hono) {
11
11
  agent?: string;
12
12
  provider?: string;
13
13
  model?: string;
14
+ toolApproval?: 'auto' | 'dangerous' | 'all';
14
15
  scope?: 'global' | 'local';
15
16
  }>();
16
17
 
@@ -19,11 +20,13 @@ export function registerDefaultsRoute(app: Hono) {
19
20
  agent: string;
20
21
  provider: string;
21
22
  model: string;
23
+ toolApproval: 'auto' | 'dangerous' | 'all';
22
24
  }> = {};
23
25
 
24
26
  if (body.agent) updates.agent = body.agent;
25
27
  if (body.provider) updates.provider = body.provider;
26
28
  if (body.model) updates.model = body.model;
29
+ if (body.toolApproval) updates.toolApproval = body.toolApproval;
27
30
 
28
31
  await setConfig(scope, updates, projectRoot);
29
32
 
@@ -52,6 +52,11 @@ export function registerMainConfigRoute(app: Hono) {
52
52
  embeddedConfig?.defaults?.model,
53
53
  cfg.defaults.model,
54
54
  ),
55
+ toolApproval: getDefault(
56
+ undefined,
57
+ embeddedConfig?.defaults?.toolApproval,
58
+ cfg.defaults.toolApproval,
59
+ ) as 'auto' | 'dangerous' | 'all',
55
60
  };
56
61
 
57
62
  return c.json({
@@ -59,7 +59,7 @@ export function registerModelsRoutes(app: Hono) {
59
59
  id: m.id,
60
60
  label: m.label || m.id,
61
61
  toolCall: m.toolCall,
62
- reasoning: m.reasoning,
62
+ reasoningText: m.reasoningText,
63
63
  vision: m.modalities?.input?.includes('image') ?? false,
64
64
  })),
65
65
  default: getDefault(
@@ -97,7 +97,7 @@ export function registerModelsRoutes(app: Hono) {
97
97
  id: string;
98
98
  label: string;
99
99
  toolCall?: boolean;
100
- reasoning?: boolean;
100
+ reasoningText?: boolean;
101
101
  }>;
102
102
  }
103
103
  > = {};
@@ -122,7 +122,7 @@ export function registerModelsRoutes(app: Hono) {
122
122
  id: m.id,
123
123
  label: m.label || m.id,
124
124
  toolCall: m.toolCall,
125
- reasoning: m.reasoning,
125
+ reasoningText: m.reasoningText,
126
126
  vision: m.modalities?.input?.includes('image') ?? false,
127
127
  })),
128
128
  };
@@ -159,7 +159,7 @@ Commit message:`;
159
159
  model,
160
160
  system: systemPrompt,
161
161
  prompt: userPrompt,
162
- maxTokens: 500,
162
+ maxOutputTokens: 500,
163
163
  });
164
164
 
165
165
  const message = text.trim();
@@ -1,4 +1,4 @@
1
- import { z } from 'zod';
1
+ import { z } from 'zod/v3';
2
2
 
3
3
  export const gitStatusSchema = z.object({
4
4
  project: z.string().optional(),
@@ -0,0 +1,53 @@
1
+ import type { Hono } from 'hono';
2
+ import {
3
+ resolveApproval,
4
+ getPendingApprovalsForSession,
5
+ } from '../runtime/tools/approval.ts';
6
+
7
+ export function registerSessionApprovalRoute(app: Hono) {
8
+ app.post('/v1/sessions/:id/approval', async (c) => {
9
+ const sessionId = c.req.param('id');
10
+ const body = await c.req.json<{
11
+ callId: string;
12
+ approved: boolean;
13
+ }>();
14
+
15
+ if (!body.callId) {
16
+ return c.json({ ok: false, error: 'callId is required' }, 400);
17
+ }
18
+
19
+ if (typeof body.approved !== 'boolean') {
20
+ return c.json({ ok: false, error: 'approved must be a boolean' }, 400);
21
+ }
22
+
23
+ console.log('[approval-route] Received approval request', {
24
+ sessionId,
25
+ callId: body.callId,
26
+ approved: body.approved,
27
+ });
28
+
29
+ const result = resolveApproval(body.callId, body.approved);
30
+
31
+ if (!result.ok) {
32
+ return c.json(result, 404);
33
+ }
34
+
35
+ return c.json({ ok: true, callId: body.callId, approved: body.approved });
36
+ });
37
+
38
+ app.get('/v1/sessions/:id/approval/pending', async (c) => {
39
+ const sessionId = c.req.param('id');
40
+ const pending = getPendingApprovalsForSession(sessionId);
41
+
42
+ return c.json({
43
+ ok: true,
44
+ pending: pending.map((p) => ({
45
+ callId: p.callId,
46
+ toolName: p.toolName,
47
+ args: p.args,
48
+ messageId: p.messageId,
49
+ createdAt: p.createdAt,
50
+ })),
51
+ });
52
+ });
53
+ }
@@ -122,7 +122,7 @@ export function registerSessionMessagesRoutes(app: Hono) {
122
122
  typeOf: typeof userContext,
123
123
  });
124
124
 
125
- const reasoning = body?.reasoning === true;
125
+ const reasoning = body?.reasoningText === true;
126
126
 
127
127
  // Validate model capabilities if tools are allowed for this agent
128
128
  const wantsToolCalls = true; // agent toolset may be non-empty
@@ -156,7 +156,7 @@ export function registerSessionMessagesRoutes(app: Hono) {
156
156
  content,
157
157
  oneShot: Boolean(body?.oneShot),
158
158
  userContext,
159
- reasoning,
159
+ reasoningText: reasoning,
160
160
  images,
161
161
  files,
162
162
  });
@@ -1,4 +1,4 @@
1
- import { loadConfig, catalog } from '@agi-cli/sdk';
1
+ import { loadConfig, getUnderlyingProviderKey } from '@agi-cli/sdk';
2
2
  import { getDb } from '@agi-cli/database';
3
3
  import { sessions } from '@agi-cli/database/schema';
4
4
  import { eq } from 'drizzle-orm';
@@ -40,16 +40,6 @@ export interface SetupResult {
40
40
 
41
41
  const THINKING_BUDGET = 16000;
42
42
 
43
- function getSolforgeUnderlyingProvider(
44
- model: string,
45
- ): 'anthropic' | 'openai' | null {
46
- const entry = catalog.solforge?.models?.find((m) => m.id === model);
47
- const npm = entry?.provider?.npm;
48
- if (npm === '@ai-sdk/anthropic') return 'anthropic';
49
- if (npm === '@ai-sdk/openai') return 'openai';
50
- return null;
51
- }
52
-
53
43
  export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
54
44
  const cfgTimer = time('runner:loadConfig+db');
55
45
  const cfg = await loadConfig(opts.projectRoot);
@@ -231,36 +221,31 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
231
221
  const providerOptions: Record<string, unknown> = {};
232
222
  let effectiveMaxOutputTokens = maxOutputTokens;
233
223
 
234
- if (opts.reasoning) {
235
- if (opts.provider === 'anthropic') {
224
+ if (opts.reasoningText) {
225
+ const underlyingProvider = getUnderlyingProviderKey(
226
+ opts.provider,
227
+ opts.model,
228
+ );
229
+
230
+ if (underlyingProvider === 'anthropic') {
236
231
  providerOptions.anthropic = {
237
232
  thinking: { type: 'enabled', budgetTokens: THINKING_BUDGET },
238
233
  };
239
234
  if (maxOutputTokens && maxOutputTokens > THINKING_BUDGET) {
240
235
  effectiveMaxOutputTokens = maxOutputTokens - THINKING_BUDGET;
241
236
  }
242
- } else if (opts.provider === 'openai') {
237
+ } else if (underlyingProvider === 'openai') {
243
238
  providerOptions.openai = {
244
239
  reasoningSummary: 'auto',
245
240
  };
246
- } else if (opts.provider === 'google') {
241
+ } else if (underlyingProvider === 'google') {
247
242
  providerOptions.google = {
248
243
  thinkingConfig: { thinkingBudget: THINKING_BUDGET },
249
244
  };
250
- } else if (opts.provider === 'solforge') {
251
- const underlying = getSolforgeUnderlyingProvider(opts.model);
252
- if (underlying === 'anthropic') {
253
- providerOptions.anthropic = {
254
- thinking: { type: 'enabled', budgetTokens: THINKING_BUDGET },
255
- };
256
- if (maxOutputTokens && maxOutputTokens > THINKING_BUDGET) {
257
- effectiveMaxOutputTokens = maxOutputTokens - THINKING_BUDGET;
258
- }
259
- } else if (underlying === 'openai') {
260
- providerOptions.openai = {
261
- reasoningSummary: 'auto',
262
- };
263
- }
245
+ } else if (underlyingProvider === 'openai-compatible') {
246
+ providerOptions['openai-compatible'] = {
247
+ reasoningEffort: 'high',
248
+ };
264
249
  }
265
250
  }
266
251
 
@@ -9,7 +9,7 @@ export interface TokenUsage {
9
9
  output: number;
10
10
  cacheRead?: number;
11
11
  cacheWrite?: number;
12
- reasoning?: number;
12
+ reasoningText?: number;
13
13
  }
14
14
 
15
15
  export interface ModelLimits {
@@ -17,7 +17,10 @@ export interface ModelLimits {
17
17
  output: number;
18
18
  }
19
19
 
20
- export function isOverflow(tokens: TokenUsage, limits: ModelLimits): boolean {
20
+ export function isOverflow(
21
+ tokens: LanguageModelUsage,
22
+ limits: ModelLimits,
23
+ ): boolean {
21
24
  if (limits.context === 0) return false;
22
25
 
23
26
  const count =
@@ -211,7 +211,7 @@ export async function buildHistoryMessages(
211
211
  }
212
212
  }
213
213
 
214
- return convertToModelMessages(ui);
214
+ return await convertToModelMessages(ui);
215
215
  }
216
216
 
217
217
  async function logPendingToolParts(
@@ -23,7 +23,7 @@ type DispatchOptions = {
23
23
  content: string;
24
24
  oneShot?: boolean;
25
25
  userContext?: string;
26
- reasoning?: boolean;
26
+ reasoningText?: boolean;
27
27
  images?: Array<{ data: string; mediaType: string }>;
28
28
  files?: Array<{
29
29
  type: 'image' | 'pdf' | 'text';
@@ -47,7 +47,7 @@ export async function dispatchAssistantMessage(
47
47
  content,
48
48
  oneShot,
49
49
  userContext,
50
- reasoning,
50
+ reasoningText,
51
51
  images,
52
52
  files,
53
53
  } = options;
@@ -171,6 +171,9 @@ export async function dispatchAssistantMessage(
171
171
  );
172
172
  }
173
173
 
174
+ // Read tool approval mode from config
175
+ const toolApprovalMode = cfg.defaults.toolApproval ?? 'auto';
176
+
174
177
  enqueueAssistantRun(
175
178
  {
176
179
  sessionId,
@@ -181,9 +184,10 @@ export async function dispatchAssistantMessage(
181
184
  projectRoot: cfg.projectRoot,
182
185
  oneShot: Boolean(oneShot),
183
186
  userContext,
184
- reasoning,
187
+ reasoningText,
185
188
  isCompactCommand: isCompact,
186
189
  compactionContext,
190
+ toolApprovalMode,
187
191
  },
188
192
  runSessionLoop,
189
193
  );
@@ -6,6 +6,7 @@ import { resolveOpenRouterModel } from './openrouter.ts';
6
6
  import { resolveSolforgeModel } from './solforge.ts';
7
7
  import { getZaiInstance, getZaiCodingInstance } from './zai.ts';
8
8
  import { resolveOpencodeModel } from './opencode.ts';
9
+ import { getMoonshotInstance } from './moonshot.ts';
9
10
 
10
11
  export type ProviderName = ProviderId;
11
12
 
@@ -42,5 +43,8 @@ export async function resolveModel(
42
43
  if (provider === 'zai-coding') {
43
44
  return getZaiCodingInstance(cfg, model);
44
45
  }
46
+ if (provider === 'moonshot') {
47
+ return getMoonshotInstance(cfg, model);
48
+ }
45
49
  throw new Error(`Unsupported provider: ${provider}`);
46
50
  }
@@ -0,0 +1,8 @@
1
+ import type { AGIConfig } from '@agi-cli/sdk';
2
+ import { getAuth, createMoonshotModel } from '@agi-cli/sdk';
3
+
4
+ export async function getMoonshotInstance(cfg: AGIConfig, model: string) {
5
+ const auth = await getAuth('moonshot', cfg.projectRoot);
6
+ const apiKey = auth?.type === 'api' ? auth.key : undefined;
7
+ return createMoonshotModel(model, { apiKey });
8
+ }
@@ -115,8 +115,8 @@ export async function createBranch({
115
115
  createdAt: msg.createdAt,
116
116
  completedAt: msg.completedAt,
117
117
  latencyMs: msg.latencyMs,
118
- promptTokens: msg.promptTokens,
119
- completionTokens: msg.completionTokens,
118
+ inputTokens: msg.inputTokens,
119
+ outputTokens: msg.outputTokens,
120
120
  totalTokens: msg.totalTokens,
121
121
  cachedInputTokens: msg.cachedInputTokens,
122
122
  cacheCreationInputTokens: msg.cacheCreationInputTokens,
@@ -27,7 +27,7 @@ export type ProviderMetadata = Record<string, unknown> & {
27
27
 
28
28
  export function normalizeUsage(
29
29
  usage: UsageData,
30
- providerMetadata: ProviderMetadata | undefined,
30
+ providerOptions: ProviderMetadata | undefined,
31
31
  provider: ProviderId,
32
32
  ): UsageData {
33
33
  const rawInputTokens = Number(usage.inputTokens ?? 0);
@@ -37,17 +37,17 @@ export function normalizeUsage(
37
37
  const cachedInputTokens =
38
38
  usage.cachedInputTokens != null
39
39
  ? Number(usage.cachedInputTokens)
40
- : providerMetadata?.openai?.cachedPromptTokens != null
41
- ? Number(providerMetadata.openai.cachedPromptTokens)
42
- : providerMetadata?.anthropic?.cacheReadInputTokens != null
43
- ? Number(providerMetadata.anthropic.cacheReadInputTokens)
40
+ : providerOptions?.openai?.cachedPromptTokens != null
41
+ ? Number(providerOptions.openai.cachedPromptTokens)
42
+ : providerOptions?.anthropic?.cacheReadInputTokens != null
43
+ ? Number(providerOptions.anthropic.cacheReadInputTokens)
44
44
  : undefined;
45
45
 
46
46
  const cacheCreationInputTokens =
47
47
  usage.cacheCreationInputTokens != null
48
48
  ? Number(usage.cacheCreationInputTokens)
49
- : providerMetadata?.anthropic?.cacheCreationInputTokens != null
50
- ? Number(providerMetadata.anthropic.cacheCreationInputTokens)
49
+ : providerOptions?.anthropic?.cacheCreationInputTokens != null
50
+ ? Number(providerOptions.anthropic.cacheCreationInputTokens)
51
51
  : undefined;
52
52
 
53
53
  const cachedValue = cachedInputTokens ?? 0;
@@ -99,18 +99,14 @@ export function resolveUsageProvider(
99
99
  */
100
100
  export async function updateSessionTokensIncremental(
101
101
  usage: UsageData,
102
- providerMetadata: ProviderMetadata | undefined,
102
+ providerOptions: ProviderMetadata | undefined,
103
103
  opts: RunOpts,
104
104
  db: Awaited<ReturnType<typeof getDb>>,
105
105
  ) {
106
106
  if (!usage || !db) return;
107
107
 
108
108
  const usageProvider = resolveUsageProvider(opts.provider, opts.model);
109
- const normalizedUsage = normalizeUsage(
110
- usage,
111
- providerMetadata,
112
- usageProvider,
113
- );
109
+ const normalizedUsage = normalizeUsage(usage, providerOptions, usageProvider);
114
110
 
115
111
  // Read session totals
116
112
  const sessRows = await db
@@ -134,8 +130,8 @@ export async function updateSessionTokensIncremental(
134
130
  .where(eq(messages.id, opts.assistantMessageId));
135
131
 
136
132
  const msg = msgRows[0];
137
- const priorPromptMsg = Number(msg?.promptTokens ?? 0);
138
- const priorCompletionMsg = Number(msg?.completionTokens ?? 0);
133
+ const priorPromptMsg = Number(msg?.inputTokens ?? 0);
134
+ const priorCompletionMsg = Number(msg?.outputTokens ?? 0);
139
135
  const priorCachedMsg = Number(msg?.cachedInputTokens ?? 0);
140
136
  const priorCacheCreationMsg = Number(msg?.cacheCreationInputTokens ?? 0);
141
137
  const priorReasoningMsg = Number(msg?.reasoningTokens ?? 0);
@@ -231,18 +227,14 @@ export async function updateSessionTokens(
231
227
  */
232
228
  export async function updateMessageTokensIncremental(
233
229
  usage: UsageData,
234
- providerMetadata: ProviderMetadata | undefined,
230
+ providerOptions: ProviderMetadata | undefined,
235
231
  opts: RunOpts,
236
232
  db: Awaited<ReturnType<typeof getDb>>,
237
233
  ) {
238
234
  if (!usage || !db) return;
239
235
 
240
236
  const usageProvider = resolveUsageProvider(opts.provider, opts.model);
241
- const normalizedUsage = normalizeUsage(
242
- usage,
243
- providerMetadata,
244
- usageProvider,
245
- );
237
+ const normalizedUsage = normalizeUsage(usage, providerOptions, usageProvider);
246
238
 
247
239
  const msgRows = await db
248
240
  .select()
@@ -251,8 +243,8 @@ export async function updateMessageTokensIncremental(
251
243
 
252
244
  if (msgRows.length > 0 && msgRows[0]) {
253
245
  const msg = msgRows[0];
254
- const priorPrompt = Number(msg.promptTokens ?? 0);
255
- const priorCompletion = Number(msg.completionTokens ?? 0);
246
+ const priorPrompt = Number(msg.inputTokens ?? 0);
247
+ const priorCompletion = Number(msg.outputTokens ?? 0);
256
248
  const priorCached = Number(msg.cachedInputTokens ?? 0);
257
249
  const priorCacheCreation = Number(msg.cacheCreationInputTokens ?? 0);
258
250
  const priorReasoning = Number(msg.reasoningTokens ?? 0);
@@ -287,8 +279,8 @@ export async function updateMessageTokensIncremental(
287
279
  await db
288
280
  .update(messages)
289
281
  .set({
290
- promptTokens: cumPrompt,
291
- completionTokens: cumCompletion,
282
+ inputTokens: cumPrompt,
283
+ outputTokens: cumCompletion,
292
284
  totalTokens: cumTotal,
293
285
  cachedInputTokens: cumCached,
294
286
  cacheCreationInputTokens: cumCacheCreation,
@@ -1,5 +1,6 @@
1
1
  import type { ProviderName } from '../provider/index.ts';
2
2
  import { publish } from '../../events/bus.ts';
3
+ import type { ToolApprovalMode } from '../tools/approval.ts';
3
4
 
4
5
  export type RunOpts = {
5
6
  sessionId: string;
@@ -10,10 +11,11 @@ export type RunOpts = {
10
11
  projectRoot: string;
11
12
  oneShot?: boolean;
12
13
  userContext?: string;
13
- reasoning?: boolean;
14
+ reasoningText?: boolean;
14
15
  abortSignal?: AbortSignal;
15
16
  isCompactCommand?: boolean;
16
17
  compactionContext?: string;
18
+ toolApprovalMode?: ToolApprovalMode;
17
19
  };
18
20
 
19
21
  export type QueuedMessage = {
@@ -73,8 +73,8 @@ export function createFinishHandler(
73
73
 
74
74
  const usage = sessRows[0]
75
75
  ? {
76
- inputTokens: Number(sessRows[0].promptTokens ?? 0),
77
- outputTokens: Number(sessRows[0].completionTokens ?? 0),
76
+ inputTokens: Number(sessRows[0].inputTokens ?? 0),
77
+ outputTokens: Number(sessRows[0].outputTokens ?? 0),
78
78
  totalTokens: Number(sessRows[0].totalTokens ?? 0),
79
79
  cachedInputTokens: Number(sessRows[0].cachedInputTokens ?? 0),
80
80
  cacheCreationInputTokens: Number(
@@ -97,7 +97,7 @@ export function createFinishHandler(
97
97
  try {
98
98
  const limits = getModelLimits(opts.provider, opts.model);
99
99
  if (limits) {
100
- const tokenUsage: TokenUsage = {
100
+ const tokenUsage: LanguageModelUsage = {
101
101
  input: usage.inputTokens ?? 0,
102
102
  output: usage.outputTokens ?? 0,
103
103
  cacheRead:
@@ -18,13 +18,13 @@ export function createStepFinishHandler(
18
18
  sharedCtx: ToolAdapterContext,
19
19
  updateSessionTokensIncrementalFn: (
20
20
  usage: UsageData,
21
- providerMetadata: ProviderMetadata | undefined,
21
+ providerOptions: ProviderMetadata | undefined,
22
22
  opts: RunOpts,
23
23
  db: Awaited<ReturnType<typeof getDb>>,
24
24
  ) => Promise<void>,
25
25
  updateMessageTokensIncrementalFn: (
26
26
  usage: UsageData,
27
- providerMetadata: ProviderMetadata | undefined,
27
+ providerOptions: ProviderMetadata | undefined,
28
28
  opts: RunOpts,
29
29
  db: Awaited<ReturnType<typeof getDb>>,
30
30
  ) => Promise<void>,
@@ -47,7 +47,7 @@ export function createStepFinishHandler(
47
47
  try {
48
48
  await updateSessionTokensIncrementalFn(
49
49
  step.usage,
50
- step.experimental_providerMetadata,
50
+ step.providerMetadata,
51
51
  opts,
52
52
  db,
53
53
  );
@@ -56,7 +56,7 @@ export function createStepFinishHandler(
56
56
  try {
57
57
  await updateMessageTokensIncrementalFn(
58
58
  step.usage,
59
- step.experimental_providerMetadata,
59
+ step.providerMetadata,
60
60
  opts,
61
61
  db,
62
62
  );
@@ -4,7 +4,7 @@ export type StepFinishEvent = {
4
4
  usage?: UsageData;
5
5
  finishReason?: string;
6
6
  response?: unknown;
7
- experimental_providerMetadata?: ProviderMetadata;
7
+ providerMetadata?: ProviderMetadata;
8
8
  };
9
9
 
10
10
  export type FinishEvent = {
@@ -0,0 +1,179 @@
1
+ import { publish } from '../../events/bus.ts';
2
+
3
+ export type ToolApprovalMode = 'auto' | 'dangerous' | 'all';
4
+
5
+ export const DANGEROUS_TOOLS = new Set([
6
+ 'bash',
7
+ 'write',
8
+ 'apply_patch',
9
+ 'terminal',
10
+ 'edit',
11
+ 'git_commit',
12
+ 'git_push',
13
+ ]);
14
+
15
+ export const SAFE_TOOLS = new Set([
16
+ 'finish',
17
+ 'progress_update',
18
+ 'update_todos',
19
+ ]);
20
+
21
+ export interface PendingApproval {
22
+ callId: string;
23
+ toolName: string;
24
+ args: unknown;
25
+ sessionId: string;
26
+ messageId: string;
27
+ resolve: (approved: boolean) => void;
28
+ createdAt: number;
29
+ }
30
+
31
+ const pendingApprovals = new Map<string, PendingApproval>();
32
+
33
+ export function requiresApproval(
34
+ toolName: string,
35
+ mode: ToolApprovalMode,
36
+ ): boolean {
37
+ if (SAFE_TOOLS.has(toolName)) return false;
38
+ if (mode === 'auto') return false;
39
+ if (mode === 'all') return true;
40
+ if (mode === 'dangerous') return DANGEROUS_TOOLS.has(toolName);
41
+ return false;
42
+ }
43
+
44
+ export async function requestApproval(
45
+ sessionId: string,
46
+ messageId: string,
47
+ callId: string,
48
+ toolName: string,
49
+ args: unknown,
50
+ timeoutMs = 120000,
51
+ ): Promise<boolean> {
52
+ console.log('[approval] requestApproval called', {
53
+ sessionId,
54
+ messageId,
55
+ callId,
56
+ toolName,
57
+ });
58
+ return new Promise((resolve) => {
59
+ const approval: PendingApproval = {
60
+ callId,
61
+ toolName,
62
+ args,
63
+ sessionId,
64
+ messageId,
65
+ resolve,
66
+ createdAt: Date.now(),
67
+ };
68
+
69
+ pendingApprovals.set(callId, approval);
70
+ console.log(
71
+ '[approval] Added to pendingApprovals, count:',
72
+ pendingApprovals.size,
73
+ );
74
+
75
+ publish({
76
+ type: 'tool.approval.required',
77
+ sessionId,
78
+ payload: {
79
+ callId,
80
+ toolName,
81
+ args,
82
+ messageId,
83
+ },
84
+ });
85
+
86
+ setTimeout(() => {
87
+ if (pendingApprovals.has(callId)) {
88
+ pendingApprovals.delete(callId);
89
+ resolve(false);
90
+ publish({
91
+ type: 'tool.approval.resolved',
92
+ sessionId,
93
+ payload: {
94
+ callId,
95
+ toolName,
96
+ approved: false,
97
+ reason: 'timeout',
98
+ },
99
+ });
100
+ }
101
+ }, timeoutMs);
102
+ });
103
+ }
104
+
105
+ export function resolveApproval(
106
+ callId: string,
107
+ approved: boolean,
108
+ ): { ok: boolean; error?: string } {
109
+ console.log('[approval] resolveApproval called', {
110
+ callId,
111
+ approved,
112
+ pendingCount: pendingApprovals.size,
113
+ pendingIds: [...pendingApprovals.keys()],
114
+ });
115
+ const approval = pendingApprovals.get(callId);
116
+ if (!approval) {
117
+ console.log('[approval] No pending approval found for callId:', callId);
118
+ return { ok: false, error: 'No pending approval found for this callId' };
119
+ }
120
+
121
+ pendingApprovals.delete(callId);
122
+ approval.resolve(approved);
123
+
124
+ publish({
125
+ type: 'tool.approval.resolved',
126
+ sessionId: approval.sessionId,
127
+ payload: {
128
+ callId,
129
+ toolName: approval.toolName,
130
+ approved,
131
+ reason: approved ? 'user_approved' : 'user_rejected',
132
+ },
133
+ });
134
+
135
+ return { ok: true };
136
+ }
137
+
138
+ export function getPendingApproval(
139
+ callId: string,
140
+ ): PendingApproval | undefined {
141
+ return pendingApprovals.get(callId);
142
+ }
143
+
144
+ export function updateApprovalArgs(callId: string, args: unknown): boolean {
145
+ const approval = pendingApprovals.get(callId);
146
+ if (!approval) return false;
147
+
148
+ approval.args = args;
149
+
150
+ publish({
151
+ type: 'tool.approval.updated',
152
+ sessionId: approval.sessionId,
153
+ payload: {
154
+ callId,
155
+ toolName: approval.toolName,
156
+ args,
157
+ messageId: approval.messageId,
158
+ },
159
+ });
160
+
161
+ return true;
162
+ }
163
+
164
+ export function getPendingApprovalsForSession(
165
+ sessionId: string,
166
+ ): PendingApproval[] {
167
+ return Array.from(pendingApprovals.values()).filter(
168
+ (a) => a.sessionId === sessionId,
169
+ );
170
+ }
171
+
172
+ export function clearPendingApprovalsForSession(sessionId: string): void {
173
+ for (const [callId, approval] of pendingApprovals) {
174
+ if (approval.sessionId === sessionId) {
175
+ approval.resolve(false);
176
+ pendingApprovals.delete(callId);
177
+ }
178
+ }
179
+ }
@@ -1,6 +1,7 @@
1
1
  import { eq } from 'drizzle-orm';
2
2
  import type { DB } from '@agi-cli/database';
3
3
  import { messageParts } from '@agi-cli/database/schema';
4
+ import type { ToolApprovalMode } from './approval.ts';
4
5
  import { publish } from '../../events/bus.ts';
5
6
 
6
7
  export type StepExecutionState = {
@@ -24,6 +25,7 @@ export type ToolAdapterContext = {
24
25
  stepExecution?: {
25
26
  states: Map<number, StepExecutionState>;
26
27
  };
28
+ toolApprovalMode?: ToolApprovalMode;
27
29
  };
28
30
 
29
31
  export function extractFinishText(input: unknown): string | undefined {
@@ -32,6 +32,7 @@ export async function setupToolContext(
32
32
  model: opts.model,
33
33
  projectRoot: opts.projectRoot,
34
34
  stepExecution: { states: new Map() },
35
+ toolApprovalMode: opts.toolApprovalMode,
35
36
  onFirstToolCall: () => {
36
37
  if (firstToolSeen) return;
37
38
  firstToolSeen = true;
@@ -13,6 +13,10 @@ import {
13
13
  toClaudeCodeName,
14
14
  requiresClaudeCodeNaming,
15
15
  } from '../runtime/tools/mapping.ts';
16
+ import {
17
+ requiresApproval,
18
+ requestApproval,
19
+ } from '../runtime/tools/approval.ts';
16
20
 
17
21
  export type { ToolAdapterContext } from '../runtime/tools/context.ts';
18
22
 
@@ -33,6 +37,7 @@ type PendingCallMeta = {
33
37
  startTs: number;
34
38
  stepIndex?: number;
35
39
  args?: unknown;
40
+ approvalPromise?: Promise<boolean>;
36
41
  };
37
42
 
38
43
  function getPendingQueue(
@@ -294,6 +299,19 @@ export function adaptTools(
294
299
  toolCallId: callId,
295
300
  });
296
301
  } catch {}
302
+ // Start approval request with full args
303
+ if (
304
+ ctx.toolApprovalMode &&
305
+ requiresApproval(name, ctx.toolApprovalMode)
306
+ ) {
307
+ meta.approvalPromise = requestApproval(
308
+ ctx.sessionId,
309
+ ctx.messageId,
310
+ callId,
311
+ name,
312
+ args,
313
+ );
314
+ }
297
315
  if (typeof base.onInputAvailable === 'function') {
298
316
  // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
299
317
  await base.onInputAvailable(options as any);
@@ -324,6 +342,16 @@ export function adaptTools(
324
342
 
325
343
  const executeWithGuards = async (): Promise<ToolExecuteReturn> => {
326
344
  try {
345
+ // Await approval if it was requested in onInputAvailable
346
+ if (meta?.approvalPromise) {
347
+ const approved = await meta.approvalPromise;
348
+ if (!approved) {
349
+ return {
350
+ ok: false,
351
+ error: 'Tool execution rejected by user',
352
+ } as ToolExecuteReturn;
353
+ }
354
+ }
327
355
  // Handle session-relative paths and cwd tools
328
356
  let res: ToolExecuteReturn | { cwd: string } | null | undefined;
329
357
  const cwd = getCwd(ctx.sessionId);
@@ -1,5 +1,5 @@
1
1
  import { tool } from 'ai';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
5
  import { eq, asc, count } from 'drizzle-orm';
@@ -1,5 +1,5 @@
1
1
  import { tool } from 'ai';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
5
  import { eq, asc, count } from 'drizzle-orm';
@@ -1,5 +1,5 @@
1
1
  import { tool } from 'ai';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
 
4
4
  const sessionLinkSchema = z.object({
5
5
  sessionId: z.string().describe('The session ID to link to'),
@@ -1,5 +1,5 @@
1
1
  import { tool } from 'ai';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
5
  import { eq, desc, asc, gte, lte, and, like, count, sql } from 'drizzle-orm';
@@ -1,5 +1,5 @@
1
1
  import { tool } from 'ai';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages } from '@agi-cli/database/schema';
5
5
  import { eq, desc, asc, gte, lte, and, sql, count } from 'drizzle-orm';
@@ -1,5 +1,5 @@
1
1
  import { tool } from 'ai';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
5
  import { eq, desc, asc, like, and, sql } from 'drizzle-orm';
package/sst-env.d.ts CHANGED
@@ -2,9 +2,7 @@
2
2
  /* tslint:disable */
3
3
  /* eslint-disable */
4
4
  /* deno-fmt-ignore-file */
5
- /* biome-ignore-all lint: auto-generated */
6
5
 
7
6
  /// <reference path="../../sst-env.d.ts" />
8
7
 
9
8
  import 'sst';
10
- export {};