@ottocode/server 0.1.173
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 +42 -0
- package/src/events/bus.ts +43 -0
- package/src/events/types.ts +32 -0
- package/src/index.ts +281 -0
- package/src/openapi/helpers.ts +64 -0
- package/src/openapi/paths/ask.ts +70 -0
- package/src/openapi/paths/config.ts +218 -0
- package/src/openapi/paths/files.ts +72 -0
- package/src/openapi/paths/git.ts +457 -0
- package/src/openapi/paths/messages.ts +92 -0
- package/src/openapi/paths/sessions.ts +90 -0
- package/src/openapi/paths/setu.ts +154 -0
- package/src/openapi/paths/stream.ts +26 -0
- package/src/openapi/paths/terminals.ts +226 -0
- package/src/openapi/schemas.ts +345 -0
- package/src/openapi/spec.ts +49 -0
- package/src/presets.ts +85 -0
- package/src/routes/ask.ts +113 -0
- package/src/routes/auth.ts +592 -0
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +44 -0
- package/src/routes/config/cwd.ts +21 -0
- package/src/routes/config/defaults.ts +45 -0
- package/src/routes/config/index.ts +16 -0
- package/src/routes/config/main.ts +73 -0
- package/src/routes/config/models.ts +139 -0
- package/src/routes/config/providers.ts +46 -0
- package/src/routes/config/utils.ts +120 -0
- package/src/routes/files.ts +218 -0
- package/src/routes/git/branch.ts +75 -0
- package/src/routes/git/commit.ts +209 -0
- package/src/routes/git/diff.ts +137 -0
- package/src/routes/git/index.ts +18 -0
- package/src/routes/git/push.ts +160 -0
- package/src/routes/git/schemas.ts +48 -0
- package/src/routes/git/staging.ts +208 -0
- package/src/routes/git/status.ts +83 -0
- package/src/routes/git/types.ts +31 -0
- package/src/routes/git/utils.ts +249 -0
- package/src/routes/openapi.ts +6 -0
- package/src/routes/research.ts +392 -0
- package/src/routes/root.ts +5 -0
- package/src/routes/session-approval.ts +63 -0
- package/src/routes/session-files.ts +387 -0
- package/src/routes/session-messages.ts +170 -0
- package/src/routes/session-stream.ts +61 -0
- package/src/routes/sessions.ts +814 -0
- package/src/routes/setu.ts +346 -0
- package/src/routes/terminals.ts +227 -0
- package/src/runtime/agent/registry.ts +351 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +257 -0
- package/src/runtime/agent/runner.ts +375 -0
- package/src/runtime/agent-registry.ts +6 -0
- package/src/runtime/ask/service.ts +369 -0
- package/src/runtime/context/environment.ts +202 -0
- package/src/runtime/debug/index.ts +117 -0
- package/src/runtime/debug/state.ts +140 -0
- package/src/runtime/errors/api-error.ts +192 -0
- package/src/runtime/errors/handling.ts +199 -0
- package/src/runtime/message/compaction-auto.ts +154 -0
- package/src/runtime/message/compaction-context.ts +101 -0
- package/src/runtime/message/compaction-detect.ts +26 -0
- package/src/runtime/message/compaction-limits.ts +37 -0
- package/src/runtime/message/compaction-mark.ts +111 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +21 -0
- package/src/runtime/message/history-builder.ts +266 -0
- package/src/runtime/message/service.ts +468 -0
- package/src/runtime/message/tool-history-tracker.ts +204 -0
- package/src/runtime/prompt/builder.ts +167 -0
- package/src/runtime/provider/anthropic.ts +50 -0
- package/src/runtime/provider/copilot.ts +12 -0
- package/src/runtime/provider/google.ts +8 -0
- package/src/runtime/provider/index.ts +60 -0
- package/src/runtime/provider/moonshot.ts +8 -0
- package/src/runtime/provider/oauth-adapter.ts +237 -0
- package/src/runtime/provider/openai.ts +18 -0
- package/src/runtime/provider/opencode.ts +7 -0
- package/src/runtime/provider/openrouter.ts +7 -0
- package/src/runtime/provider/selection.ts +118 -0
- package/src/runtime/provider/setu.ts +126 -0
- package/src/runtime/provider/zai.ts +16 -0
- package/src/runtime/session/branch.ts +280 -0
- package/src/runtime/session/db-operations.ts +285 -0
- package/src/runtime/session/manager.ts +99 -0
- package/src/runtime/session/queue.ts +243 -0
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +371 -0
- package/src/runtime/stream/finish-handler.ts +101 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +25 -0
- package/src/runtime/tools/approval.ts +180 -0
- package/src/runtime/tools/context.ts +83 -0
- package/src/runtime/tools/mapping.ts +154 -0
- package/src/runtime/tools/setup.ts +44 -0
- package/src/runtime/topup/manager.ts +110 -0
- package/src/runtime/utils/cwd.ts +69 -0
- package/src/runtime/utils/token.ts +35 -0
- package/src/tools/adapter.ts +634 -0
- package/src/tools/database/get-parent-session.ts +183 -0
- package/src/tools/database/get-session-context.ts +161 -0
- package/src/tools/database/index.ts +42 -0
- package/src/tools/database/present-session-links.ts +47 -0
- package/src/tools/database/query-messages.ts +160 -0
- package/src/tools/database/query-sessions.ts +126 -0
- package/src/tools/database/search-history.ts +135 -0
- package/src/types/sql-imports.d.ts +5 -0
- package/sst-env.d.ts +8 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime debug state management
|
|
3
|
+
*
|
|
4
|
+
* Centralizes debug flag state that can be set either via:
|
|
5
|
+
* - Environment variables (OTTO_DEBUG, DEBUG_OTTO)
|
|
6
|
+
* - Runtime configuration (CLI --debug flag)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
10
|
+
|
|
11
|
+
type DebugState = {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
traceEnabled: boolean;
|
|
14
|
+
runtimeOverride: boolean | null;
|
|
15
|
+
runtimeTraceOverride: boolean | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Global state
|
|
19
|
+
const state: DebugState = {
|
|
20
|
+
enabled: false,
|
|
21
|
+
traceEnabled: false,
|
|
22
|
+
runtimeOverride: null,
|
|
23
|
+
runtimeTraceOverride: null,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type GlobalDebugFlags = {
|
|
27
|
+
__OTTO_DEBUG_ENABLED__?: boolean;
|
|
28
|
+
__OTTO_TRACE_ENABLED__?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const globalFlags = globalThis as GlobalDebugFlags;
|
|
32
|
+
|
|
33
|
+
function syncGlobalFlags() {
|
|
34
|
+
globalFlags.__OTTO_DEBUG_ENABLED__ = state.enabled;
|
|
35
|
+
globalFlags.__OTTO_TRACE_ENABLED__ = state.traceEnabled;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if environment variables indicate debug mode
|
|
40
|
+
*/
|
|
41
|
+
function checkEnvDebug(): boolean {
|
|
42
|
+
const sources = [process.env.OTTO_DEBUG, process.env.DEBUG_OTTO];
|
|
43
|
+
for (const value of sources) {
|
|
44
|
+
if (!value) continue;
|
|
45
|
+
const trimmed = value.trim().toLowerCase();
|
|
46
|
+
if (TRUTHY.has(trimmed) || trimmed === 'all') {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if environment variables indicate trace mode
|
|
55
|
+
*/
|
|
56
|
+
function checkEnvTrace(): boolean {
|
|
57
|
+
const sources = [process.env.OTTO_TRACE, process.env.TRACE_OTTO];
|
|
58
|
+
for (const value of sources) {
|
|
59
|
+
if (!value) continue;
|
|
60
|
+
const trimmed = value.trim().toLowerCase();
|
|
61
|
+
if (TRUTHY.has(trimmed)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize debug state from environment
|
|
70
|
+
*/
|
|
71
|
+
function initialize() {
|
|
72
|
+
if (state.runtimeOverride === null) {
|
|
73
|
+
state.enabled = checkEnvDebug();
|
|
74
|
+
}
|
|
75
|
+
if (state.runtimeTraceOverride === null) {
|
|
76
|
+
state.traceEnabled = checkEnvTrace();
|
|
77
|
+
}
|
|
78
|
+
syncGlobalFlags();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if debug mode is enabled
|
|
83
|
+
* Considers both runtime override and environment variables
|
|
84
|
+
*/
|
|
85
|
+
export function isDebugEnabled(): boolean {
|
|
86
|
+
initialize();
|
|
87
|
+
return state.enabled;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if trace mode is enabled (shows stack traces)
|
|
92
|
+
* Trace mode requires debug mode to be enabled
|
|
93
|
+
*/
|
|
94
|
+
export function isTraceEnabled(): boolean {
|
|
95
|
+
initialize();
|
|
96
|
+
return state.enabled && state.traceEnabled;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Enable or disable debug mode at runtime
|
|
101
|
+
* Overrides environment variable settings
|
|
102
|
+
*
|
|
103
|
+
* @param enabled - true to enable debug mode, false to disable
|
|
104
|
+
*/
|
|
105
|
+
export function setDebugEnabled(enabled: boolean): void {
|
|
106
|
+
state.enabled = enabled;
|
|
107
|
+
state.runtimeOverride = enabled;
|
|
108
|
+
syncGlobalFlags();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Enable or disable trace mode at runtime
|
|
113
|
+
* Trace mode shows full stack traces in error logs
|
|
114
|
+
*
|
|
115
|
+
* @param enabled - true to enable trace mode, false to disable
|
|
116
|
+
*/
|
|
117
|
+
export function setTraceEnabled(enabled: boolean): void {
|
|
118
|
+
state.traceEnabled = enabled;
|
|
119
|
+
state.runtimeTraceOverride = enabled;
|
|
120
|
+
syncGlobalFlags();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reset debug state to environment defaults
|
|
125
|
+
*/
|
|
126
|
+
export function resetDebugState(): void {
|
|
127
|
+
state.runtimeOverride = null;
|
|
128
|
+
state.runtimeTraceOverride = null;
|
|
129
|
+
state.enabled = checkEnvDebug();
|
|
130
|
+
state.traceEnabled = checkEnvTrace();
|
|
131
|
+
syncGlobalFlags();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get current debug state (for testing/diagnostics)
|
|
136
|
+
*/
|
|
137
|
+
export function getDebugState(): Readonly<DebugState> {
|
|
138
|
+
initialize();
|
|
139
|
+
return { ...state };
|
|
140
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified API error handling
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error serialization and response formatting
|
|
5
|
+
* across all API endpoints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isDebugEnabled } from '../debug/state.ts';
|
|
9
|
+
import { toErrorPayload } from './handling.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Standard API error response format
|
|
13
|
+
*/
|
|
14
|
+
export type APIErrorResponse = {
|
|
15
|
+
error: {
|
|
16
|
+
message: string;
|
|
17
|
+
type: string;
|
|
18
|
+
code?: string;
|
|
19
|
+
status?: number;
|
|
20
|
+
details?: Record<string, unknown>;
|
|
21
|
+
stack?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom API Error class
|
|
27
|
+
*/
|
|
28
|
+
export class APIError extends Error {
|
|
29
|
+
public readonly code?: string;
|
|
30
|
+
public readonly status: number;
|
|
31
|
+
public readonly type: string;
|
|
32
|
+
public readonly details?: Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
message: string,
|
|
36
|
+
options?: {
|
|
37
|
+
code?: string;
|
|
38
|
+
status?: number;
|
|
39
|
+
type?: string;
|
|
40
|
+
details?: Record<string, unknown>;
|
|
41
|
+
cause?: unknown;
|
|
42
|
+
},
|
|
43
|
+
) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'APIError';
|
|
46
|
+
this.code = options?.code;
|
|
47
|
+
this.status = options?.status ?? 500;
|
|
48
|
+
this.type = options?.type ?? 'api_error';
|
|
49
|
+
this.details = options?.details;
|
|
50
|
+
|
|
51
|
+
if (options?.cause) {
|
|
52
|
+
this.cause = options.cause;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Maintain proper stack trace
|
|
56
|
+
if (Error.captureStackTrace) {
|
|
57
|
+
Error.captureStackTrace(this, APIError);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Serialize any error into a consistent API error response
|
|
64
|
+
*
|
|
65
|
+
* @param err - The error to serialize
|
|
66
|
+
* @returns A properly formatted API error response
|
|
67
|
+
*/
|
|
68
|
+
export function serializeError(err: unknown): APIErrorResponse {
|
|
69
|
+
// Use existing error payload logic
|
|
70
|
+
const payload = toErrorPayload(err);
|
|
71
|
+
|
|
72
|
+
// Determine HTTP status code
|
|
73
|
+
// Default to 400 for generic errors (client errors)
|
|
74
|
+
// Only use 500 if explicitly set or for APIError instances without a status
|
|
75
|
+
let status = 400;
|
|
76
|
+
|
|
77
|
+
// Handle APIError instances first
|
|
78
|
+
if (err instanceof APIError) {
|
|
79
|
+
status = err.status;
|
|
80
|
+
} else if (err && typeof err === 'object') {
|
|
81
|
+
const errObj = err as Record<string, unknown>;
|
|
82
|
+
if (typeof errObj.status === 'number') {
|
|
83
|
+
status = errObj.status;
|
|
84
|
+
} else if (typeof errObj.statusCode === 'number') {
|
|
85
|
+
status = errObj.statusCode;
|
|
86
|
+
} else if (
|
|
87
|
+
errObj.details &&
|
|
88
|
+
typeof errObj.details === 'object' &&
|
|
89
|
+
typeof (errObj.details as Record<string, unknown>).statusCode === 'number'
|
|
90
|
+
) {
|
|
91
|
+
status = (errObj.details as Record<string, unknown>).statusCode as number;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Extract code if available
|
|
96
|
+
let code: string | undefined;
|
|
97
|
+
if (err && typeof err === 'object') {
|
|
98
|
+
const errObj = err as Record<string, unknown>;
|
|
99
|
+
if (typeof errObj.code === 'string') {
|
|
100
|
+
code = errObj.code;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (err instanceof APIError && err.code) {
|
|
105
|
+
code = err.code;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Build response
|
|
109
|
+
const response: APIErrorResponse = {
|
|
110
|
+
error: {
|
|
111
|
+
message: payload.message || 'An error occurred',
|
|
112
|
+
type: payload.type || 'unknown',
|
|
113
|
+
status,
|
|
114
|
+
...(code ? { code } : {}),
|
|
115
|
+
...(payload.details ? { details: payload.details } : {}),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Include stack trace in debug mode
|
|
120
|
+
if (isDebugEnabled() && err instanceof Error && err.stack) {
|
|
121
|
+
response.error.stack = err.stack;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create an error response with proper HTTP status code
|
|
129
|
+
*
|
|
130
|
+
* @param err - The error to convert
|
|
131
|
+
* @returns Tuple of [APIErrorResponse, HTTP status code]
|
|
132
|
+
*/
|
|
133
|
+
export function createErrorResponse(err: unknown): [APIErrorResponse, number] {
|
|
134
|
+
const response = serializeError(err);
|
|
135
|
+
return [response, response.error.status ?? 500];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Normalize error to ensure it's an Error instance
|
|
140
|
+
*
|
|
141
|
+
* @param err - The error to normalize
|
|
142
|
+
* @returns An Error instance
|
|
143
|
+
*/
|
|
144
|
+
export function normalizeError(err: unknown): Error {
|
|
145
|
+
if (err instanceof Error) {
|
|
146
|
+
return err;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof err === 'string') {
|
|
150
|
+
return new Error(err);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (err && typeof err === 'object') {
|
|
154
|
+
const errObj = err as Record<string, unknown>;
|
|
155
|
+
if (typeof errObj.message === 'string') {
|
|
156
|
+
return new Error(errObj.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return new Error('An unknown error occurred');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract error message from any error type
|
|
165
|
+
*
|
|
166
|
+
* @param err - The error to extract message from
|
|
167
|
+
* @returns The error message string
|
|
168
|
+
*/
|
|
169
|
+
export function getErrorMessage(err: unknown): string {
|
|
170
|
+
if (typeof err === 'string') {
|
|
171
|
+
return err;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (err instanceof Error) {
|
|
175
|
+
return err.message;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (err && typeof err === 'object') {
|
|
179
|
+
const errObj = err as Record<string, unknown>;
|
|
180
|
+
if (typeof errObj.message === 'string') {
|
|
181
|
+
return errObj.message;
|
|
182
|
+
}
|
|
183
|
+
if (typeof errObj.error === 'string') {
|
|
184
|
+
return errObj.error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return 'An unknown error occurred';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Legacy compatibility - AskServiceError alias
|
|
192
|
+
export { APIError as AskServiceError };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { APICallError } from 'ai';
|
|
2
|
+
|
|
3
|
+
export type ErrorPayload = {
|
|
4
|
+
message: string;
|
|
5
|
+
type: string;
|
|
6
|
+
details?: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function toErrorPayload(err: unknown): ErrorPayload {
|
|
10
|
+
let actualError = err;
|
|
11
|
+
if (
|
|
12
|
+
err &&
|
|
13
|
+
typeof err === 'object' &&
|
|
14
|
+
'error' in err &&
|
|
15
|
+
Object.keys(err).length === 1
|
|
16
|
+
) {
|
|
17
|
+
actualError = (err as { error: unknown }).error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const asObj =
|
|
21
|
+
actualError && typeof actualError === 'object'
|
|
22
|
+
? (actualError as Record<string, unknown>)
|
|
23
|
+
: undefined;
|
|
24
|
+
let message = '';
|
|
25
|
+
let errorType = 'unknown';
|
|
26
|
+
const details: Record<string, unknown> = {};
|
|
27
|
+
|
|
28
|
+
if (APICallError.isInstance(actualError)) {
|
|
29
|
+
errorType = 'api_error';
|
|
30
|
+
message = actualError.message || 'API call failed';
|
|
31
|
+
|
|
32
|
+
details.name = actualError.name;
|
|
33
|
+
details.statusCode = actualError.statusCode;
|
|
34
|
+
details.url = actualError.url;
|
|
35
|
+
details.isRetryable = actualError.isRetryable;
|
|
36
|
+
|
|
37
|
+
if (actualError.responseBody) {
|
|
38
|
+
details.responseBody = actualError.responseBody;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(actualError.responseBody);
|
|
41
|
+
if (parsed.error) {
|
|
42
|
+
if (typeof parsed.error === 'string') {
|
|
43
|
+
message = parsed.error;
|
|
44
|
+
} else if (parsed.error.message) {
|
|
45
|
+
message = parsed.error.message;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (parsed.error?.type) {
|
|
49
|
+
details.apiErrorType = parsed.error.type;
|
|
50
|
+
}
|
|
51
|
+
if (parsed.error?.code) {
|
|
52
|
+
details.apiErrorCode = parsed.error.code;
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (actualError.requestBodyValues) {
|
|
58
|
+
details.requestBodyValues = actualError.requestBodyValues;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (actualError.responseHeaders) {
|
|
62
|
+
details.responseHeaders = actualError.responseHeaders;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (actualError.cause) {
|
|
66
|
+
const cause = actualError.cause as Record<string, unknown> | undefined;
|
|
67
|
+
details.cause = {
|
|
68
|
+
message: typeof cause?.message === 'string' ? cause.message : undefined,
|
|
69
|
+
code: cause?.code,
|
|
70
|
+
status: cause?.status ?? cause?.statusCode,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { message, type: errorType, details };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
asObj &&
|
|
79
|
+
'type' in asObj &&
|
|
80
|
+
asObj.type === 'error' &&
|
|
81
|
+
'error' in asObj &&
|
|
82
|
+
typeof asObj.error === 'object' &&
|
|
83
|
+
asObj.error
|
|
84
|
+
) {
|
|
85
|
+
const errorObj = asObj.error as Record<string, unknown>;
|
|
86
|
+
|
|
87
|
+
if (typeof errorObj.message === 'string') {
|
|
88
|
+
message = errorObj.message;
|
|
89
|
+
}
|
|
90
|
+
if (typeof errorObj.type === 'string') {
|
|
91
|
+
errorType = errorObj.type;
|
|
92
|
+
details.errorType = errorObj.type;
|
|
93
|
+
}
|
|
94
|
+
if (typeof errorObj.code === 'string') {
|
|
95
|
+
details.code = errorObj.code;
|
|
96
|
+
}
|
|
97
|
+
if ('param' in errorObj) {
|
|
98
|
+
details.param = errorObj.param;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { message, type: errorType, details };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (asObj) {
|
|
105
|
+
if ('name' in asObj && typeof asObj.name === 'string') {
|
|
106
|
+
errorType = asObj.name;
|
|
107
|
+
details.name = asObj.name;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ('type' in asObj && typeof asObj.type === 'string') {
|
|
111
|
+
errorType = asObj.type;
|
|
112
|
+
details.type = asObj.type;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ('code' in asObj && asObj.code != null) {
|
|
116
|
+
details.code = asObj.code;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ('status' in asObj && asObj.status != null) {
|
|
120
|
+
details.status = asObj.status;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if ('statusCode' in asObj && asObj.statusCode != null) {
|
|
124
|
+
details.statusCode = asObj.statusCode;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (asObj && typeof asObj.message === 'string' && asObj.message) {
|
|
129
|
+
message = asObj.message;
|
|
130
|
+
} else if (typeof actualError === 'string') {
|
|
131
|
+
message = actualError;
|
|
132
|
+
} else if (asObj && typeof asObj.error === 'string' && asObj.error) {
|
|
133
|
+
message = asObj.error;
|
|
134
|
+
} else if (
|
|
135
|
+
asObj &&
|
|
136
|
+
typeof asObj.responseBody === 'string' &&
|
|
137
|
+
asObj.responseBody
|
|
138
|
+
) {
|
|
139
|
+
details.responseBody = asObj.responseBody;
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(asObj.responseBody);
|
|
142
|
+
if (parsed.error) {
|
|
143
|
+
if (typeof parsed.error === 'string') {
|
|
144
|
+
message = parsed.error;
|
|
145
|
+
} else if (typeof parsed.error.message === 'string') {
|
|
146
|
+
message = parsed.error.message;
|
|
147
|
+
} else {
|
|
148
|
+
message = asObj.responseBody;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
message = asObj.responseBody;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
message = asObj.responseBody;
|
|
155
|
+
}
|
|
156
|
+
} else if (asObj?.statusCode && asObj.url) {
|
|
157
|
+
message = `HTTP ${String(asObj.statusCode)} error at ${String(asObj.url)}`;
|
|
158
|
+
details.url = asObj.url;
|
|
159
|
+
} else if (asObj?.name) {
|
|
160
|
+
message = String(asObj.name);
|
|
161
|
+
} else {
|
|
162
|
+
message = 'An error occurred';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (asObj) {
|
|
166
|
+
if ('url' in asObj) details.url = asObj.url;
|
|
167
|
+
if ('isRetryable' in asObj) details.isRetryable = asObj.isRetryable;
|
|
168
|
+
if ('data' in asObj) details.data = asObj.data;
|
|
169
|
+
|
|
170
|
+
if (asObj.cause) {
|
|
171
|
+
const c = asObj.cause as Record<string, unknown> | undefined;
|
|
172
|
+
details.cause = {
|
|
173
|
+
message: typeof c?.message === 'string' ? c.message : undefined,
|
|
174
|
+
code: c?.code,
|
|
175
|
+
status: c?.status ?? c?.statusCode,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
(asObj as { response?: { status?: unknown; statusText?: unknown } })
|
|
181
|
+
?.response?.status
|
|
182
|
+
) {
|
|
183
|
+
details.response = {
|
|
184
|
+
status: (
|
|
185
|
+
asObj as { response?: { status?: unknown; statusText?: unknown } }
|
|
186
|
+
).response?.status,
|
|
187
|
+
statusText: (
|
|
188
|
+
asObj as { response?: { status?: unknown; statusText?: unknown } }
|
|
189
|
+
).response?.statusText,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
message,
|
|
196
|
+
type: errorType,
|
|
197
|
+
details: Object.keys(details).length ? details : undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { getDb } from '@ottocode/database';
|
|
2
|
+
import { messageParts } from '@ottocode/database/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { streamText } from 'ai';
|
|
5
|
+
import { resolveModel } from '../provider/index.ts';
|
|
6
|
+
import { getAuth } from '@ottocode/sdk';
|
|
7
|
+
import { loadConfig } from '@ottocode/sdk';
|
|
8
|
+
import { debugLog } from '../debug/index.ts';
|
|
9
|
+
import { getModelLimits } from './compaction-limits.ts';
|
|
10
|
+
import { buildCompactionContext } from './compaction-context.ts';
|
|
11
|
+
import { getCompactionSystemPrompt } from './compaction-detect.ts';
|
|
12
|
+
import { markSessionCompacted } from './compaction-mark.ts';
|
|
13
|
+
import { detectOAuth, adaptSimpleCall } from '../provider/oauth-adapter.ts';
|
|
14
|
+
|
|
15
|
+
export async function performAutoCompaction(
|
|
16
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
17
|
+
sessionId: string,
|
|
18
|
+
assistantMessageId: string,
|
|
19
|
+
publishFn: (event: {
|
|
20
|
+
type: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
payload: Record<string, unknown>;
|
|
23
|
+
}) => void,
|
|
24
|
+
provider: string,
|
|
25
|
+
modelId: string,
|
|
26
|
+
): Promise<{
|
|
27
|
+
success: boolean;
|
|
28
|
+
summary?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
compactMessageId?: string;
|
|
31
|
+
}> {
|
|
32
|
+
debugLog(`[compaction] Starting auto-compaction for session ${sessionId}`);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const limits = getModelLimits(provider, modelId);
|
|
36
|
+
const contextTokenLimit = limits
|
|
37
|
+
? Math.max(Math.floor(limits.context * 0.5), 15000)
|
|
38
|
+
: 15000;
|
|
39
|
+
debugLog(
|
|
40
|
+
`[compaction] Model ${modelId} context limit: ${limits?.context ?? 'unknown'}, using ${contextTokenLimit} tokens for compaction`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const context = await buildCompactionContext(
|
|
44
|
+
db,
|
|
45
|
+
sessionId,
|
|
46
|
+
contextTokenLimit,
|
|
47
|
+
);
|
|
48
|
+
if (!context || context.length < 100) {
|
|
49
|
+
debugLog('[compaction] Not enough context to compact');
|
|
50
|
+
return { success: false, error: 'Not enough context to compact' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cfg = await loadConfig();
|
|
54
|
+
debugLog(
|
|
55
|
+
`[compaction] Using session model ${provider}/${modelId} for auto-compaction`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const auth = await getAuth(
|
|
59
|
+
provider as Parameters<typeof getAuth>[0],
|
|
60
|
+
cfg.projectRoot,
|
|
61
|
+
);
|
|
62
|
+
const oauth = detectOAuth(provider, auth);
|
|
63
|
+
|
|
64
|
+
debugLog(
|
|
65
|
+
`[compaction] OAuth: needsSpoof=${oauth.needsSpoof}, isOpenAIOAuth=${oauth.isOpenAIOAuth}`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const model = await resolveModel(
|
|
69
|
+
provider as Parameters<typeof resolveModel>[0],
|
|
70
|
+
modelId,
|
|
71
|
+
cfg,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const compactionPrompt = getCompactionSystemPrompt();
|
|
75
|
+
const userContent = `IMPORTANT: Generate a comprehensive summary. This will replace the detailed conversation history.\n\nPlease summarize this conversation:\n\n<conversation-to-summarize>\n${context}\n</conversation-to-summarize>`;
|
|
76
|
+
|
|
77
|
+
const adapted = adaptSimpleCall(oauth, {
|
|
78
|
+
instructions: compactionPrompt,
|
|
79
|
+
userContent,
|
|
80
|
+
maxOutputTokens: 2000,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const compactPartId = crypto.randomUUID();
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
|
|
86
|
+
await db.insert(messageParts).values({
|
|
87
|
+
id: compactPartId,
|
|
88
|
+
messageId: assistantMessageId,
|
|
89
|
+
index: 0,
|
|
90
|
+
stepIndex: 0,
|
|
91
|
+
type: 'text',
|
|
92
|
+
content: JSON.stringify({ text: '' }),
|
|
93
|
+
agent: 'system',
|
|
94
|
+
provider: provider,
|
|
95
|
+
model: modelId,
|
|
96
|
+
startedAt: now,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = streamText({
|
|
100
|
+
model,
|
|
101
|
+
system: adapted.system,
|
|
102
|
+
messages: adapted.messages,
|
|
103
|
+
maxOutputTokens: adapted.maxOutputTokens,
|
|
104
|
+
providerOptions: adapted.providerOptions,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
let summary = '';
|
|
108
|
+
for await (const chunk of result.textStream) {
|
|
109
|
+
summary += chunk;
|
|
110
|
+
|
|
111
|
+
publishFn({
|
|
112
|
+
type: 'message.part.delta',
|
|
113
|
+
sessionId,
|
|
114
|
+
payload: {
|
|
115
|
+
messageId: assistantMessageId,
|
|
116
|
+
partId: compactPartId,
|
|
117
|
+
stepIndex: 0,
|
|
118
|
+
type: 'text',
|
|
119
|
+
delta: chunk,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await db
|
|
125
|
+
.update(messageParts)
|
|
126
|
+
.set({
|
|
127
|
+
content: JSON.stringify({ text: summary }),
|
|
128
|
+
completedAt: Date.now(),
|
|
129
|
+
})
|
|
130
|
+
.where(eq(messageParts.id, compactPartId));
|
|
131
|
+
|
|
132
|
+
if (!summary || summary.length < 50) {
|
|
133
|
+
debugLog('[compaction] Failed to generate summary');
|
|
134
|
+
return { success: false, error: 'Failed to generate summary' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
debugLog(`[compaction] Generated summary: ${summary.slice(0, 100)}...`);
|
|
138
|
+
|
|
139
|
+
const compactResult = await markSessionCompacted(
|
|
140
|
+
db,
|
|
141
|
+
sessionId,
|
|
142
|
+
assistantMessageId,
|
|
143
|
+
);
|
|
144
|
+
debugLog(
|
|
145
|
+
`[compaction] Marked ${compactResult.compacted} parts as compacted, saved ~${compactResult.saved} tokens`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return { success: true, summary, compactMessageId: assistantMessageId };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
debugLog(`[compaction] Auto-compaction failed: ${errorMsg}`);
|
|
152
|
+
return { success: false, error: errorMsg };
|
|
153
|
+
}
|
|
154
|
+
}
|