@ottocode/server 0.1.221 → 0.1.223
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/package.json +3 -3
- package/src/openapi/paths/sessions.ts +25 -0
- package/src/openapi/schemas.ts +1 -0
- package/src/routes/sessions.ts +37 -0
- package/src/runtime/agent/oauth-codex-continuation.ts +16 -9
- package/src/runtime/agent/runner.ts +53 -40
- package/src/runtime/provider/index.ts +1 -1
- package/src/runtime/provider/openai.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.223",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/sdk": "0.1.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.223",
|
|
53
|
+
"@ottocode/database": "0.1.223",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.3.6"
|
|
@@ -78,6 +78,31 @@ export const sessionsPaths = {
|
|
|
78
78
|
},
|
|
79
79
|
},
|
|
80
80
|
'/v1/sessions/{sessionId}': {
|
|
81
|
+
get: {
|
|
82
|
+
tags: ['sessions'],
|
|
83
|
+
operationId: 'getSession',
|
|
84
|
+
summary: 'Get a single session by ID',
|
|
85
|
+
parameters: [
|
|
86
|
+
{
|
|
87
|
+
in: 'path',
|
|
88
|
+
name: 'sessionId',
|
|
89
|
+
required: true,
|
|
90
|
+
schema: { type: 'string' },
|
|
91
|
+
},
|
|
92
|
+
projectQueryParam(),
|
|
93
|
+
],
|
|
94
|
+
responses: {
|
|
95
|
+
200: {
|
|
96
|
+
description: 'OK',
|
|
97
|
+
content: {
|
|
98
|
+
'application/json': {
|
|
99
|
+
schema: { $ref: '#/components/schemas/Session' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
404: errorResponse(),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
81
106
|
patch: {
|
|
82
107
|
tags: ['sessions'],
|
|
83
108
|
operationId: 'updateSession',
|
package/src/openapi/schemas.ts
CHANGED
|
@@ -66,6 +66,7 @@ export const schemas = {
|
|
|
66
66
|
totalOutputTokens: { type: 'integer', nullable: true },
|
|
67
67
|
totalCachedTokens: { type: 'integer', nullable: true },
|
|
68
68
|
totalCacheCreationTokens: { type: 'integer', nullable: true },
|
|
69
|
+
currentContextTokens: { type: 'integer', nullable: true },
|
|
69
70
|
totalToolTimeMs: { type: 'integer', nullable: true },
|
|
70
71
|
toolCounts: {
|
|
71
72
|
type: 'object',
|
package/src/routes/sessions.ts
CHANGED
|
@@ -104,6 +104,43 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
104
104
|
}
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
// Get single session
|
|
108
|
+
app.get('/v1/sessions/:sessionId', async (c) => {
|
|
109
|
+
try {
|
|
110
|
+
const sessionId = c.req.param('sessionId');
|
|
111
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
112
|
+
const cfg = await loadConfig(projectRoot);
|
|
113
|
+
const db = await getDb(cfg.projectRoot);
|
|
114
|
+
const rows = await db
|
|
115
|
+
.select()
|
|
116
|
+
.from(sessions)
|
|
117
|
+
.where(eq(sessions.id, sessionId))
|
|
118
|
+
.limit(1);
|
|
119
|
+
if (!rows.length) {
|
|
120
|
+
return c.json(
|
|
121
|
+
{ error: { message: 'Session not found', status: 404 } },
|
|
122
|
+
404,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const r = rows[0];
|
|
126
|
+
let counts: Record<string, unknown> | undefined;
|
|
127
|
+
if (r.toolCountsJson) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(r.toolCountsJson);
|
|
130
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
131
|
+
counts = parsed as Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
const { toolCountsJson: _toolCountsJson, ...rest } = r;
|
|
136
|
+
return c.json(counts ? { ...rest, toolCounts: counts } : rest);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logger.error('Failed to get session', err);
|
|
139
|
+
const errorResponse = serializeError(err);
|
|
140
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
107
144
|
// Update session preferences
|
|
108
145
|
app.patch('/v1/sessions/:sessionId', async (c) => {
|
|
109
146
|
try {
|
|
@@ -24,10 +24,6 @@ const INTERMEDIATE_PROGRESS_PATTERNS: RegExp[] = [
|
|
|
24
24
|
/\b(and|then)\s+continue\b/i,
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Detects whether assistant text looks like an intermediate progress update
|
|
29
|
-
* (e.g. "Next I'll inspect...") rather than a final user-facing completion.
|
|
30
|
-
*/
|
|
31
27
|
export function looksLikeIntermediateProgressText(text: string): boolean {
|
|
32
28
|
const trimmed = text.trim();
|
|
33
29
|
if (!trimmed) return false;
|
|
@@ -44,11 +40,15 @@ function isTruncatedResponse(
|
|
|
44
40
|
return rawFinishReason === 'max_output_tokens';
|
|
45
41
|
}
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
const MAX_UNCLEAN_EOF_RETRIES = 1;
|
|
44
|
+
|
|
45
|
+
function isUncleanEof(input: OauthCodexContinuationInput): boolean {
|
|
46
|
+
if (input.finishReason && input.finishReason !== 'unknown') return false;
|
|
47
|
+
if (input.firstToolSeen) return true;
|
|
48
|
+
if (looksLikeIntermediateProgressText(input.lastAssistantText)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
52
|
export function decideOauthCodexContinuation(
|
|
53
53
|
input: OauthCodexContinuationInput,
|
|
54
54
|
): OauthCodexContinuationDecision {
|
|
@@ -68,5 +68,12 @@ export function decideOauthCodexContinuation(
|
|
|
68
68
|
return { shouldContinue: true, reason: 'truncated' };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
if (
|
|
72
|
+
isUncleanEof(input) &&
|
|
73
|
+
input.continuationCount < MAX_UNCLEAN_EOF_RETRIES
|
|
74
|
+
) {
|
|
75
|
+
return { shouldContinue: true, reason: 'unclean-eof' };
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
return { shouldContinue: false };
|
|
72
79
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hasToolCall, stepCountIs, streamText } from 'ai';
|
|
2
|
-
import { messages, messageParts } from '@ottocode/database/schema';
|
|
2
|
+
import { messages, messageParts, sessions } from '@ottocode/database/schema';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
4
|
import { publish, subscribe } from '../../events/bus.ts';
|
|
5
5
|
import { debugLog, time } from '../debug/index.ts';
|
|
@@ -243,7 +243,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
243
243
|
|
|
244
244
|
const onFinish = createFinishHandler(opts, db, completeAssistantMessage);
|
|
245
245
|
const stopWhenCondition = isOpenAIOAuth
|
|
246
|
-
? stepCountIs(
|
|
246
|
+
? stepCountIs(20)
|
|
247
247
|
: hasToolCall('finish');
|
|
248
248
|
|
|
249
249
|
try {
|
|
@@ -423,55 +423,68 @@ async function runAssistant(opts: RunOpts) {
|
|
|
423
423
|
});
|
|
424
424
|
|
|
425
425
|
if (continuationDecision.shouldContinue) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
426
|
+
const sessRows = await db
|
|
427
|
+
.select()
|
|
428
|
+
.from(sessions)
|
|
429
|
+
.where(eq(sessions.id, opts.sessionId))
|
|
430
|
+
.limit(1);
|
|
431
|
+
const sessionInputTokens = Number(sessRows[0]?.totalInputTokens ?? 0);
|
|
432
|
+
const MAX_SESSION_INPUT_TOKENS = 800_000;
|
|
433
|
+
if (sessionInputTokens > MAX_SESSION_INPUT_TOKENS) {
|
|
434
|
+
debugLog(
|
|
435
|
+
`[RUNNER] Token budget exceeded (${sessionInputTokens} > ${MAX_SESSION_INPUT_TOKENS}), stopping continuation.`,
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
debugLog(
|
|
439
|
+
`[RUNNER] WARNING: Stream ended without finish. reason=${continuationDecision.reason ?? 'unknown'}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}, firstToolSeen=${fs}. Auto-continuing.`,
|
|
440
|
+
);
|
|
433
441
|
|
|
434
|
-
try {
|
|
435
|
-
await completeAssistantMessage({}, opts, db);
|
|
436
|
-
} catch (err) {
|
|
437
442
|
debugLog(
|
|
438
|
-
`[RUNNER]
|
|
443
|
+
`[RUNNER] Auto-continuing (${continuationCount + 1}/${MAX_CONTINUATIONS})...`,
|
|
439
444
|
);
|
|
440
|
-
}
|
|
441
445
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
provider: opts.provider,
|
|
450
|
-
model: opts.model,
|
|
451
|
-
createdAt: Date.now(),
|
|
452
|
-
});
|
|
446
|
+
try {
|
|
447
|
+
await completeAssistantMessage({}, opts, db);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
debugLog(
|
|
450
|
+
`[RUNNER] completeAssistantMessage failed before continuation: ${err instanceof Error ? err.message : String(err)}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
453
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
sessionId: opts.sessionId,
|
|
457
|
-
payload: {
|
|
454
|
+
const continuationMessageId = crypto.randomUUID();
|
|
455
|
+
await db.insert(messages).values({
|
|
458
456
|
id: continuationMessageId,
|
|
457
|
+
sessionId: opts.sessionId,
|
|
459
458
|
role: 'assistant',
|
|
459
|
+
status: 'pending',
|
|
460
460
|
agent: opts.agent,
|
|
461
461
|
provider: opts.provider,
|
|
462
462
|
model: opts.model,
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
createdAt: Date.now(),
|
|
464
|
+
});
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
466
|
+
publish({
|
|
467
|
+
type: 'message.created',
|
|
468
|
+
sessionId: opts.sessionId,
|
|
469
|
+
payload: {
|
|
470
|
+
id: continuationMessageId,
|
|
471
|
+
role: 'assistant',
|
|
472
|
+
agent: opts.agent,
|
|
473
|
+
provider: opts.provider,
|
|
474
|
+
model: opts.model,
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
enqueueAssistantRun(
|
|
479
|
+
{
|
|
480
|
+
...opts,
|
|
481
|
+
assistantMessageId: continuationMessageId,
|
|
482
|
+
continuationCount: continuationCount + 1,
|
|
483
|
+
},
|
|
484
|
+
runSessionLoop,
|
|
485
|
+
);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
475
488
|
}
|
|
476
489
|
if (
|
|
477
490
|
continuationDecision.reason === 'max-continuations-reached' &&
|
|
@@ -25,7 +25,7 @@ export async function resolveModel(
|
|
|
25
25
|
},
|
|
26
26
|
) {
|
|
27
27
|
if (provider === 'openai') {
|
|
28
|
-
return resolveOpenAIModel(model, cfg);
|
|
28
|
+
return resolveOpenAIModel(model, cfg, options?.sessionId);
|
|
29
29
|
}
|
|
30
30
|
if (provider === 'anthropic') {
|
|
31
31
|
const instance = await getAnthropicInstance(cfg);
|
|
@@ -2,12 +2,17 @@ import type { OttoConfig } from '@ottocode/sdk';
|
|
|
2
2
|
import { getAuth, createOpenAIOAuthModel } from '@ottocode/sdk';
|
|
3
3
|
import { openai, createOpenAI } from '@ai-sdk/openai';
|
|
4
4
|
|
|
5
|
-
export async function resolveOpenAIModel(
|
|
5
|
+
export async function resolveOpenAIModel(
|
|
6
|
+
model: string,
|
|
7
|
+
cfg: OttoConfig,
|
|
8
|
+
sessionId?: string,
|
|
9
|
+
) {
|
|
6
10
|
const auth = await getAuth('openai', cfg.projectRoot);
|
|
7
11
|
if (auth?.type === 'oauth') {
|
|
8
12
|
return createOpenAIOAuthModel(model, {
|
|
9
13
|
oauth: auth,
|
|
10
14
|
projectRoot: cfg.projectRoot,
|
|
15
|
+
sessionId,
|
|
11
16
|
});
|
|
12
17
|
}
|
|
13
18
|
if (auth?.type === 'api' && auth.key) {
|