@capekai/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +12 -0
  2. package/package.json +105 -0
  3. package/src/adapters/ai-sdk.ts +84 -0
  4. package/src/compaction/contracts.ts +82 -0
  5. package/src/compaction/executor.ts +161 -0
  6. package/src/compaction/policy.ts +318 -0
  7. package/src/compaction/recovery.ts +139 -0
  8. package/src/compaction/task.ts +540 -0
  9. package/src/configuration/contracts.ts +58 -0
  10. package/src/configuration/defaults.ts +27 -0
  11. package/src/configuration/runtime.ts +42 -0
  12. package/src/configuration/single-model.ts +75 -0
  13. package/src/context/assembler.ts +112 -0
  14. package/src/context/index.ts +2 -0
  15. package/src/context/sources.ts +119 -0
  16. package/src/context/workspace.ts +63 -0
  17. package/src/core/agent.ts +401 -0
  18. package/src/core/build-tools.ts +139 -0
  19. package/src/core/chat-handler.ts +858 -0
  20. package/src/core/error-handling.ts +18 -0
  21. package/src/core/fork.ts +103 -0
  22. package/src/core/interrupt.ts +192 -0
  23. package/src/core/message-utils.ts +261 -0
  24. package/src/core/model-utils.ts +149 -0
  25. package/src/core/part-utils.ts +88 -0
  26. package/src/core/provider-utils.ts +67 -0
  27. package/src/core/revert.ts +46 -0
  28. package/src/core/step-handlers.ts +157 -0
  29. package/src/core/stream/finalization.ts +65 -0
  30. package/src/core/stream/stream-config.ts +82 -0
  31. package/src/core/stream-handlers.ts +242 -0
  32. package/src/core/structured-output.ts +68 -0
  33. package/src/core/tool-builders/agent-tools.ts +71 -0
  34. package/src/core/tool-builders/external-tools.ts +179 -0
  35. package/src/core/tool-builders/types.ts +16 -0
  36. package/src/core/tool-builders/workspace-tools.ts +293 -0
  37. package/src/core/tool-capabilities.ts +65 -0
  38. package/src/goals/evaluator.ts +171 -0
  39. package/src/goals/index.ts +3 -0
  40. package/src/goals/loop.ts +167 -0
  41. package/src/goals/service.ts +39 -0
  42. package/src/index.ts +10 -0
  43. package/src/internal/ask-authority.ts +29 -0
  44. package/src/internal/composition.ts +44 -0
  45. package/src/internal/configuration.ts +22 -0
  46. package/src/internal/execution.ts +108 -0
  47. package/src/internal/hosts.ts +64 -0
  48. package/src/internal/plugins.ts +71 -0
  49. package/src/internal/providers.ts +32 -0
  50. package/src/internal/sandbox.ts +19 -0
  51. package/src/internal/tools.ts +48 -0
  52. package/src/internal/workspace.ts +25 -0
  53. package/src/kernel/diagnostics.ts +249 -0
  54. package/src/kernel/errors.ts +120 -0
  55. package/src/kernel/events.ts +82 -0
  56. package/src/kernel/index.ts +72 -0
  57. package/src/kernel/kernel.ts +62 -0
  58. package/src/kernel/lifecycle.ts +72 -0
  59. package/src/kernel/plugin.ts +218 -0
  60. package/src/kernel/registry.ts +493 -0
  61. package/src/kernel/scope.ts +776 -0
  62. package/src/kernel/service-key.ts +19 -0
  63. package/src/kernel/types.ts +317 -0
  64. package/src/memory/index.ts +2 -0
  65. package/src/memory/memory-tool.ts +75 -0
  66. package/src/memory/registry.ts +172 -0
  67. package/src/permission/ask-user-api.ts +70 -0
  68. package/src/permission/contracts.ts +135 -0
  69. package/src/permission/permission-request-manager.ts +58 -0
  70. package/src/permission/policy.ts +277 -0
  71. package/src/permission/runtime.ts +612 -0
  72. package/src/plugins/compaction-policy.ts +46 -0
  73. package/src/plugins/compose.ts +171 -0
  74. package/src/plugins/context-sections.ts +246 -0
  75. package/src/plugins/default-agent-driver.ts +14 -0
  76. package/src/plugins/facade-plugins.ts +129 -0
  77. package/src/plugins/goal-domain.ts +82 -0
  78. package/src/plugins/legacy-system-message.ts +152 -0
  79. package/src/plugins/loaded-tools.ts +23 -0
  80. package/src/plugins/memory-domain.ts +264 -0
  81. package/src/plugins/orchestrator-session.ts +29 -0
  82. package/src/plugins/permission-policy.ts +49 -0
  83. package/src/plugins/retry-policy.ts +28 -0
  84. package/src/plugins/scheduler-domain.ts +192 -0
  85. package/src/plugins/service-keys.ts +294 -0
  86. package/src/plugins/session-search-domain.ts +238 -0
  87. package/src/plugins/skills-domain.ts +272 -0
  88. package/src/plugins/subagent-domain.ts +287 -0
  89. package/src/plugins/tool-catalog.ts +78 -0
  90. package/src/plugins/tool-output-policy.ts +52 -0
  91. package/src/plugins/value-plugins.ts +150 -0
  92. package/src/plugins/workflow-domain.ts +198 -0
  93. package/src/plugins/workspace-policy.ts +37 -0
  94. package/src/providers/registry.ts +63 -0
  95. package/src/providers/types.ts +44 -0
  96. package/src/retry/policy.ts +282 -0
  97. package/src/retry/stream-chat.ts +312 -0
  98. package/src/runtime/agent-runtime.ts +83 -0
  99. package/src/runtime/default-agent-driver.ts +23 -0
  100. package/src/runtime/domain-tool-source.ts +156 -0
  101. package/src/runtime/events.ts +61 -0
  102. package/src/runtime/host-dependencies.ts +71 -0
  103. package/src/runtime/host-guidance.ts +22 -0
  104. package/src/runtime/host-layout.ts +23 -0
  105. package/src/runtime/host.ts +129 -0
  106. package/src/runtime/standalone-host.ts +118 -0
  107. package/src/sandbox/controller.ts +204 -0
  108. package/src/sandbox/model.ts +305 -0
  109. package/src/sandbox/provider.ts +53 -0
  110. package/src/sandbox/types.ts +110 -0
  111. package/src/scheduler/host.ts +22 -0
  112. package/src/scheduler/scheduler-tool.ts +172 -0
  113. package/src/session-search/host.ts +56 -0
  114. package/src/session-search/index.ts +23 -0
  115. package/src/session-search/session-search-tool.ts +151 -0
  116. package/src/skills/index.ts +3 -0
  117. package/src/skills/registry.ts +63 -0
  118. package/src/skills/skill-manage-tool.ts +205 -0
  119. package/src/skills/skill-tool.ts +42 -0
  120. package/src/storage/contracts.ts +159 -0
  121. package/src/storage/memory.ts +321 -0
  122. package/src/storage/options.ts +75 -0
  123. package/src/storage/runtime.ts +115 -0
  124. package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
  125. package/src/storage/sqlite.ts +321 -0
  126. package/src/storage/tool-output-artifacts.ts +75 -0
  127. package/src/storage.ts +31 -0
  128. package/src/subagent/child-session.ts +282 -0
  129. package/src/subagent/guidance.ts +8 -0
  130. package/src/subagent/policy.ts +198 -0
  131. package/src/subagent/task-tool.ts +584 -0
  132. package/src/tool-output/contracts.ts +111 -0
  133. package/src/tool-output/policy.ts +410 -0
  134. package/src/tool.ts +1 -0
  135. package/src/tools/executor.ts +258 -0
  136. package/src/tools/install-manifest.ts +40 -0
  137. package/src/tools/llm-api.ts +77 -0
  138. package/src/tools/registry.ts +206 -0
  139. package/src/tools/tool-artifact.ts +182 -0
  140. package/src/tools/tool-source.ts +53 -0
  141. package/src/utils/errors.ts +334 -0
  142. package/src/utils/strip-visualization.ts +50 -0
  143. package/src/workflow/decomposer.ts +139 -0
  144. package/src/workflow/execution.ts +523 -0
  145. package/src/workflow/orchestrator-session.ts +161 -0
  146. package/src/workflow/synthesizer.ts +130 -0
  147. package/src/workspace/contracts.ts +135 -0
  148. package/src/workspace/policy.ts +327 -0
@@ -0,0 +1,149 @@
1
+ import { type LanguageModel } from 'ai';
2
+ import { createOpenAI } from '@ai-sdk/openai';
3
+ import { findProviderFromModel, parseModelSpecifier } from './provider-utils';
4
+ import { findModel, getApiKeyForProvider, getLLMBaseUrl, getModelsConfig } from '../configuration/runtime';
5
+ import { createModelForProvider, getProvider } from '../providers/registry';
6
+ import { isSandboxActive } from '../runtime/host-dependencies';
7
+
8
+ export interface ModelWithMetadata {
9
+ model: LanguageModel;
10
+ useProviderInstructions?: boolean;
11
+ omitMaxOutputTokens?: boolean;
12
+ providerOptions?: Record<string, Record<string, unknown>>;
13
+ }
14
+
15
+ export interface ModelResolutionOptions {
16
+ modelId?: string;
17
+ providerId?: string;
18
+ systemPrompt?: string;
19
+ sessionId?: string;
20
+ }
21
+
22
+ export async function getModelWithMetadata(options: ModelResolutionOptions): Promise<ModelWithMetadata>;
23
+ export async function getModelWithMetadata(modelId?: string, providerId?: string, systemPrompt?: string): Promise<ModelWithMetadata>;
24
+ export async function getModelWithMetadata(
25
+ modelIdOrOptions?: string | ModelResolutionOptions,
26
+ providerId?: string,
27
+ systemPrompt?: string,
28
+ ): Promise<ModelWithMetadata> {
29
+ const options: ModelResolutionOptions = typeof modelIdOrOptions === 'string'
30
+ ? { modelId: modelIdOrOptions, providerId, systemPrompt }
31
+ : (modelIdOrOptions ?? {});
32
+ const requestedModelId = options.modelId || getModelsConfig().defaultModel;
33
+ const parsedSpecifier = parseModelSpecifier(requestedModelId);
34
+ const resolvedModelId = parsedSpecifier.modelId;
35
+ const sandboxProvider = getProvider('sandbox');
36
+
37
+ // When sandbox mode is active, route all LLM calls through the sandbox provider
38
+ if (sandboxProvider && isSandboxActive()) {
39
+ const result = await createModelForProvider({
40
+ modelId: resolvedModelId,
41
+ providerId: 'sandbox',
42
+ systemPrompt: options.systemPrompt || '',
43
+ sessionId: options.sessionId,
44
+ });
45
+ return {
46
+ model: result.model,
47
+ useProviderInstructions: result.useProviderInstructions,
48
+ omitMaxOutputTokens: result.omitMaxOutputTokens,
49
+ providerOptions: result.providerOptions,
50
+ };
51
+ }
52
+
53
+ let provider = options.providerId ?? parsedSpecifier.providerId;
54
+ let model = resolvedModelId;
55
+
56
+ if (!provider) {
57
+ provider = findProviderFromModel(resolvedModelId);
58
+ const modelInfo = findModel(resolvedModelId);
59
+ if (modelInfo) {
60
+ model = modelInfo.id;
61
+ }
62
+ }
63
+
64
+ if (!provider) {
65
+ throw new Error('No provider resolved for model "' + model + '". Configure runtime configuration (getModelsConfig) with a default provider, register the provider, or pass providerId explicitly.');
66
+ }
67
+
68
+ const registeredProvider = getProvider(provider);
69
+ if (registeredProvider) {
70
+ const result = await createModelForProvider({
71
+ modelId: model,
72
+ providerId: provider,
73
+ systemPrompt: options.systemPrompt || '',
74
+ sessionId: options.sessionId,
75
+ });
76
+ return {
77
+ model: result.model,
78
+ useProviderInstructions: result.useProviderInstructions,
79
+ omitMaxOutputTokens: result.omitMaxOutputTokens,
80
+ providerOptions: result.providerOptions,
81
+ };
82
+ }
83
+
84
+ const apiKey = getApiKeyForProvider(provider);
85
+
86
+ if (!apiKey) {
87
+ throw new Error(`No API key configured for provider: ${provider}. Register the provider with registerProvider or configure getApiKey in runtime configuration.`);
88
+ }
89
+
90
+ switch (provider) {
91
+ case 'openrouter': {
92
+ const { createOpenRouter } = await import('@openrouter/ai-sdk-provider');
93
+ const openrouter = createOpenRouter({ apiKey });
94
+ return { model: openrouter.chat(model) as unknown as LanguageModel };
95
+ }
96
+
97
+ case 'minimax': {
98
+ const { createMinimax } = await import('vercel-minimax-ai-provider');
99
+ const minimax = createMinimax({ apiKey });
100
+ return { model: minimax.chat(model) as unknown as LanguageModel };
101
+ }
102
+
103
+ case 'zhipu': {
104
+ const { createZhipu } = await import('zhipu-ai-provider');
105
+ const zhipu = createZhipu({
106
+ apiKey,
107
+ baseURL: 'https://open.bigmodel.cn/api/paas/v4',
108
+ });
109
+ return { model: zhipu.chat(model) as unknown as LanguageModel };
110
+ }
111
+
112
+ case 'zhipu-coding': {
113
+ const { createZhipu } = await import('zhipu-ai-provider');
114
+ const zhipu = createZhipu({
115
+ apiKey,
116
+ baseURL: 'https://api.z.ai/api/coding/paas/v4',
117
+ });
118
+ return { model: zhipu.chat(model) as unknown as LanguageModel };
119
+ }
120
+
121
+ case 'deepseek': {
122
+ const { createDeepSeek } = await import('@ai-sdk/deepseek');
123
+ const deepseek = createDeepSeek({ apiKey });
124
+ return { model: deepseek.chat(model) as unknown as LanguageModel };
125
+ }
126
+
127
+ case 'openai':
128
+ default: {
129
+ const openai = createOpenAI({
130
+ apiKey,
131
+ baseURL: getLLMBaseUrl() || undefined,
132
+ });
133
+ return {
134
+ model: openai.responses(model) as unknown as LanguageModel,
135
+ providerOptions: {
136
+ openai: {
137
+ promptCacheKey: options.sessionId,
138
+ store: false,
139
+ },
140
+ },
141
+ };
142
+ }
143
+ }
144
+ }
145
+
146
+ export async function getModel(modelId?: string, providerId?: string): Promise<LanguageModel> {
147
+ const { model } = await getModelWithMetadata({ modelId, providerId });
148
+ return model;
149
+ }
@@ -0,0 +1,88 @@
1
+ import type { TextPart, ToolPart, StepPart, Part, ImagePart, FilePart } from '@capekai/types';
2
+ import { randomUUID } from 'crypto';
3
+
4
+ export function isTextPart(part: Part): part is TextPart {
5
+ return part.type === 'text';
6
+ }
7
+
8
+ export function isToolPart(part: Part): part is ToolPart {
9
+ return part.type === 'tool';
10
+ }
11
+
12
+ export function isImagePart(part: Part): part is ImagePart {
13
+ return part.type === 'image';
14
+ }
15
+
16
+ export function isFilePart(part: Part): part is FilePart {
17
+ return part.type === 'file';
18
+ }
19
+
20
+ const NULL_BYTE = '\u0000';
21
+
22
+ function stripNullBytes(value: string): string {
23
+ if (!value.includes(NULL_BYTE)) {
24
+ return value;
25
+ }
26
+ return value.split(NULL_BYTE).join('');
27
+ }
28
+
29
+ function sanitizeInput(input: unknown): unknown {
30
+ if (typeof input === 'string') {
31
+ return stripNullBytes(input);
32
+ }
33
+ if (Array.isArray(input)) {
34
+ return input.map(sanitizeInput);
35
+ }
36
+ if (input !== null && typeof input === 'object') {
37
+ const result: Record<string, unknown> = {};
38
+ for (const key of Object.keys(input)) {
39
+ result[stripNullBytes(key)] = sanitizeInput((input as Record<string, unknown>)[key]);
40
+ }
41
+ return result;
42
+ }
43
+ return input;
44
+ }
45
+
46
+ export function parseToolInput(input: unknown): Record<string, unknown> {
47
+ if (input === null || input === undefined) {
48
+ return {};
49
+ }
50
+ if (typeof input === 'string') {
51
+ try {
52
+ const parsed = JSON.parse(input);
53
+ if (typeof parsed === 'object' && parsed !== null) {
54
+ return sanitizeInput(parsed) as Record<string, unknown>;
55
+ }
56
+ return {};
57
+ } catch {
58
+ return {};
59
+ }
60
+ }
61
+ return typeof input === 'object'
62
+ ? sanitizeInput(input) as Record<string, unknown>
63
+ : {};
64
+ }
65
+
66
+ export function createStepPart(options: {
67
+ messageId: string;
68
+ sessionId: string;
69
+ number: number;
70
+ status: 'started' | 'finished';
71
+ finishReason?: 'stop' | 'tool-calls' | 'error' | 'length';
72
+ tokens?: { prompt: number; completion: number };
73
+ cost?: number;
74
+ snapshot?: string;
75
+ }): StepPart {
76
+ return {
77
+ id: randomUUID(),
78
+ messageId: options.messageId,
79
+ createdAt: Date.now(),
80
+ type: 'step',
81
+ number: options.number,
82
+ status: options.status,
83
+ ...(options.finishReason && { finishReason: options.finishReason }),
84
+ ...(options.tokens && { tokens: options.tokens }),
85
+ ...(options.cost !== undefined && { cost: options.cost }),
86
+ ...(options.snapshot && { snapshot: options.snapshot }),
87
+ };
88
+ }
@@ -0,0 +1,67 @@
1
+ import { findModel, getModelsConfig } from '../configuration/runtime';
2
+ import { getProvider } from '../providers/registry';
3
+ import type { Session, Preconfig } from '@capekai/types';
4
+
5
+ export type Provider = 'openai' | 'openrouter' | 'minimax' | 'zhipu' | 'zhipu-coding' | 'deepseek';
6
+
7
+ const EXPLICIT_PROVIDERS = new Set<Provider>([
8
+ 'openai',
9
+ 'openrouter',
10
+ 'minimax',
11
+ 'zhipu',
12
+ 'zhipu-coding',
13
+ 'deepseek',
14
+ ]);
15
+
16
+ const PROVIDER_PREFIXES: Array<{ test: (m: string) => boolean; provider: string }> = [
17
+ { test: (m) => m.includes('/'), provider: 'openrouter' },
18
+ { test: (m) => m.startsWith('MiniMax-') || m.toLowerCase().includes('minimax'), provider: 'minimax' },
19
+ { test: (m) => m.startsWith('deepseek-'), provider: 'deepseek' },
20
+ ];
21
+
22
+ export interface ParsedModelSpecifier {
23
+ modelId: string;
24
+ providerId?: string;
25
+ }
26
+
27
+ export function parseModelSpecifier(modelId: string): ParsedModelSpecifier {
28
+ const separator = modelId.indexOf('/');
29
+ if (separator <= 0 || separator === modelId.length - 1) return { modelId };
30
+ const providerId = modelId.slice(0, separator) as Provider;
31
+ if (!EXPLICIT_PROVIDERS.has(providerId) && !getProvider(providerId)) return { modelId };
32
+ return {
33
+ providerId,
34
+ modelId: modelId.slice(separator + 1),
35
+ };
36
+ }
37
+
38
+ export function findProviderFromModel(modelId: string): string {
39
+ const modelInfo = findModel(modelId);
40
+ if (modelInfo) return modelInfo.providerId;
41
+
42
+ const parsed = parseModelSpecifier(modelId);
43
+ if (parsed.providerId) return parsed.providerId;
44
+
45
+ for (const { test, provider } of PROVIDER_PREFIXES) {
46
+ if (test(modelId)) return provider;
47
+ }
48
+ return getModelsConfig().defaultProvider;
49
+ }
50
+
51
+ export function resolveModelId(
52
+ session: Pick<Session, 'selectedModel'> | null,
53
+ preconfig: Pick<Preconfig, 'model'> | null | undefined,
54
+ ): string {
55
+ return session?.selectedModel || preconfig?.model || getModelsConfig().defaultModel;
56
+ }
57
+
58
+ export function resolveProviderId(
59
+ session: Pick<Session, 'selectedProvider'> | null,
60
+ preconfig: Pick<Preconfig, 'model'> | null | undefined,
61
+ ): string {
62
+ return (
63
+ session?.selectedProvider ||
64
+ (preconfig?.model ? findProviderFromModel(preconfig.model) : null) ||
65
+ getModelsConfig().defaultProvider
66
+ );
67
+ }
@@ -0,0 +1,46 @@
1
+ import { deleteMessage, listMessagesWithParts, updateMessage } from '../storage/runtime';
2
+
3
+ interface RevertResult {
4
+ revertedTo: { messageId: string | null; messageCount: number };
5
+ removed: { messageIds: string[]; partCount: number };
6
+ }
7
+
8
+ interface RevertOptions {
9
+ sessionId: string;
10
+ targetMessageId: string;
11
+ keepTarget?: boolean;
12
+ }
13
+
14
+ export async function revertToStep(options: RevertOptions): Promise<RevertResult> {
15
+ const { sessionId, targetMessageId, keepTarget = false } = options;
16
+ const allMessages = await listMessagesWithParts(sessionId);
17
+ const targetIndex = allMessages.findIndex((entry) => entry.message.id === targetMessageId);
18
+ if (targetIndex === -1) throw new Error('Target message not found');
19
+
20
+ const messagesToDelete = targetIndex === 0 && !keepTarget
21
+ ? allMessages
22
+ : allMessages.slice(targetIndex + 1);
23
+ const removedMessageIds: string[] = [];
24
+ let partCountRemoved = 0;
25
+
26
+ for (const { message, parts } of messagesToDelete) {
27
+ partCountRemoved += parts.length;
28
+ removedMessageIds.push(message.id);
29
+ await deleteMessage(message.id);
30
+ }
31
+
32
+ for (const { message } of await listMessagesWithParts(sessionId)) {
33
+ if (message.role === 'assistant' && message.status === 'streaming') {
34
+ await updateMessage(message.id, { status: 'error', error: 'Reverted before completion' });
35
+ }
36
+ }
37
+
38
+ const clearedAll = targetIndex === 0 && !keepTarget;
39
+ return {
40
+ revertedTo: {
41
+ messageId: clearedAll ? null : targetMessageId,
42
+ messageCount: clearedAll ? 0 : targetIndex,
43
+ },
44
+ removed: { messageIds: removedMessageIds, partCount: partCountRemoved },
45
+ };
46
+ }
@@ -0,0 +1,157 @@
1
+ import type { MessageEvent, StepPart } from '@capekai/types';
2
+ import { createPart, updatePart } from '../storage/runtime';
3
+ import { createStepPart } from './part-utils';
4
+ import { randomUUID } from 'crypto';
5
+ import { getModelsConfig } from '../configuration/runtime';
6
+
7
+ interface StepUsage {
8
+ inputTokens?: number;
9
+ outputTokens?: number;
10
+ totalTokens?: number;
11
+ inputTokenDetails?: {
12
+ cacheReadTokens?: number;
13
+ cacheWriteTokens?: number;
14
+ noCacheTokens?: number;
15
+ };
16
+ }
17
+
18
+ export interface UsageEventData {
19
+ promptTokens: number;
20
+ completionTokens: number;
21
+ totalTokens: number;
22
+ cacheReadTokens: number;
23
+ cacheWriteTokens: number;
24
+ noCacheTokens: number;
25
+ }
26
+
27
+ export interface StepCallbacksContext {
28
+ messageId: string;
29
+ sessionId: string;
30
+ stepParts: StepPart[];
31
+ yieldFn: ((event: CallbackEvent) => void) | null;
32
+ isMainSession: boolean | null;
33
+ contextWindow: number | undefined;
34
+ autoThreshold: number;
35
+ resolvedModelId: string | undefined;
36
+ variant: string | undefined;
37
+ needsCompaction: boolean;
38
+ latestUsage: UsageEventData;
39
+ }
40
+
41
+ export type CallbackEvent = MessageEvent | {
42
+ type: 'usage';
43
+ usage: UsageEventData;
44
+ model: string;
45
+ variant: string | null;
46
+ };
47
+
48
+ export function createStepCallbacks(ctx: StepCallbacksContext) {
49
+ return {
50
+ experimental_onStepStart: async (stepStartEvent: { stepNumber: number }) => {
51
+ const stepNumber = stepStartEvent.stepNumber + 1;
52
+
53
+ const startedStepPart = createStepPart({
54
+ messageId: ctx.messageId,
55
+ sessionId: ctx.sessionId,
56
+ number: stepNumber,
57
+ status: 'started',
58
+ });
59
+ ctx.stepParts.push(startedStepPart);
60
+
61
+ if (ctx.yieldFn) {
62
+ ctx.yieldFn({ type: 'part.created', sessionId: ctx.sessionId, part: startedStepPart });
63
+ }
64
+ await createPart(startedStepPart, ctx.sessionId);
65
+ },
66
+ onStepFinish: async (stepFinishEvent: { stepNumber: number; finishReason: string | null; usage?: StepUsage; totalUsage?: StepUsage }) => {
67
+ const stepNumber = stepFinishEvent.stepNumber + 1;
68
+
69
+ const stepUsage = stepFinishEvent.usage;
70
+ const stepPromptTokens = stepUsage?.inputTokens ?? 0;
71
+ const stepCompletionTokens = stepUsage?.outputTokens ?? 0;
72
+ const stepCacheReadTokens = stepUsage?.inputTokenDetails?.cacheReadTokens ?? 0;
73
+ const stepCacheWriteTokens = stepUsage?.inputTokenDetails?.cacheWriteTokens ?? 0;
74
+ const stepNoCacheTokens = stepUsage?.inputTokenDetails?.noCacheTokens ?? 0;
75
+
76
+ if (ctx.isMainSession && ctx.contextWindow) {
77
+ const latestStepInputTokens = stepUsage?.inputTokens ?? 0;
78
+ if (latestStepInputTokens >= ctx.autoThreshold) {
79
+ ctx.needsCompaction = true;
80
+ }
81
+ }
82
+
83
+ let finishReason: 'stop' | 'tool-calls' | 'error' | 'length' | undefined;
84
+ if (stepFinishEvent.finishReason) {
85
+ if (stepFinishEvent.finishReason === 'stop') {
86
+ finishReason = 'stop';
87
+ } else if (stepFinishEvent.finishReason === 'tool-calls') {
88
+ finishReason = 'tool-calls';
89
+ } else if (stepFinishEvent.finishReason === 'length') {
90
+ finishReason = 'length';
91
+ } else if (stepFinishEvent.finishReason === 'error' || stepFinishEvent.finishReason === 'other') {
92
+ finishReason = 'error';
93
+ }
94
+ }
95
+
96
+ const existingStepPart = ctx.stepParts.find(sp => sp.number === stepNumber);
97
+
98
+ const finishedStepPart: StepPart = {
99
+ id: existingStepPart?.id || randomUUID(),
100
+ messageId: ctx.messageId,
101
+ createdAt: existingStepPart?.createdAt || Date.now(),
102
+ type: 'step',
103
+ number: stepNumber,
104
+ status: 'finished',
105
+ finishReason,
106
+ tokens: {
107
+ prompt: stepPromptTokens,
108
+ completion: stepCompletionTokens,
109
+ cacheRead: stepCacheReadTokens,
110
+ cacheWrite: stepCacheWriteTokens,
111
+ noCache: stepNoCacheTokens,
112
+ },
113
+ };
114
+
115
+ if (existingStepPart) {
116
+ const index = ctx.stepParts.indexOf(existingStepPart);
117
+ ctx.stepParts[index] = finishedStepPart;
118
+ } else {
119
+ ctx.stepParts.push(finishedStepPart);
120
+ }
121
+
122
+ if (ctx.yieldFn) {
123
+ ctx.yieldFn({ type: 'part.updated', sessionId: ctx.sessionId, part: finishedStepPart });
124
+ }
125
+ await updatePart(finishedStepPart.id, {
126
+ status: finishedStepPart.status,
127
+ finishReason: finishedStepPart.finishReason,
128
+ tokens: finishedStepPart.tokens,
129
+ });
130
+
131
+ if (stepUsage) {
132
+ ctx.latestUsage.promptTokens = stepUsage.inputTokens ?? 0;
133
+ ctx.latestUsage.completionTokens = stepUsage.outputTokens ?? 0;
134
+ ctx.latestUsage.totalTokens = stepUsage.totalTokens ?? 0;
135
+ ctx.latestUsage.cacheReadTokens = stepUsage.inputTokenDetails?.cacheReadTokens ?? 0;
136
+ ctx.latestUsage.cacheWriteTokens = stepUsage.inputTokenDetails?.cacheWriteTokens ?? 0;
137
+ ctx.latestUsage.noCacheTokens = stepUsage.inputTokenDetails?.noCacheTokens ?? 0;
138
+
139
+ if (ctx.yieldFn) {
140
+ ctx.yieldFn({
141
+ type: 'usage',
142
+ usage: {
143
+ promptTokens: ctx.latestUsage.promptTokens,
144
+ completionTokens: ctx.latestUsage.completionTokens,
145
+ totalTokens: ctx.latestUsage.totalTokens,
146
+ cacheReadTokens: ctx.latestUsage.cacheReadTokens,
147
+ cacheWriteTokens: ctx.latestUsage.cacheWriteTokens,
148
+ noCacheTokens: ctx.latestUsage.noCacheTokens,
149
+ },
150
+ model: ctx.resolvedModelId ?? getModelsConfig().defaultModel,
151
+ variant: ctx.variant ?? null,
152
+ });
153
+ }
154
+ }
155
+ },
156
+ };
157
+ }
@@ -0,0 +1,65 @@
1
+ import type { LanguageModelUsage } from 'ai';
2
+ import { extractJsonFromText } from '../structured-output';
3
+ import type { StructuredOutputData, ResponseFormat } from '@capekai/types';
4
+
5
+ export interface FinalizationData {
6
+ usageData: LanguageModelUsage | null;
7
+ structuredOutputData: StructuredOutputData | undefined;
8
+ }
9
+
10
+ export interface FinalizationOptions {
11
+ result: {
12
+ totalUsage: PromiseLike<LanguageModelUsage>;
13
+ usage: PromiseLike<LanguageModelUsage | undefined>;
14
+ output: PromiseLike<unknown>;
15
+ };
16
+ responseFormat?: ResponseFormat;
17
+ usePromptBasedStructuredOutput: boolean;
18
+ accumulatedText: string;
19
+ }
20
+
21
+ export async function extractFinalizationData(options: FinalizationOptions): Promise<FinalizationData> {
22
+ const { result, responseFormat, usePromptBasedStructuredOutput, accumulatedText } = options;
23
+
24
+ let usageData: LanguageModelUsage | null = null;
25
+ let structuredOutputData: StructuredOutputData | undefined;
26
+
27
+ try {
28
+ const totalUsagePromise = result.totalUsage;
29
+ const usagePromise = result.usage;
30
+ const [totalUsage, usage] = await Promise.all([totalUsagePromise, usagePromise]);
31
+ usageData = usage ?? totalUsage;
32
+
33
+ if (responseFormat) {
34
+ try {
35
+ if (usePromptBasedStructuredOutput) {
36
+ const parsed = extractJsonFromText(accumulatedText);
37
+ if (parsed) {
38
+ structuredOutputData = {
39
+ formatName: responseFormat.name,
40
+ data: parsed,
41
+ schema: responseFormat.schema,
42
+ };
43
+ } else {
44
+ console.warn('Failed to parse structured output from text response');
45
+ }
46
+ } else {
47
+ const output = await result.output;
48
+ if (output && typeof output === 'object') {
49
+ structuredOutputData = {
50
+ formatName: responseFormat.name,
51
+ data: output as Record<string, unknown>,
52
+ schema: responseFormat.schema,
53
+ };
54
+ }
55
+ }
56
+ } catch (_outputErr) {
57
+ console.warn('Failed to get structured output:', _outputErr);
58
+ }
59
+ }
60
+ } catch (_usageErr) {
61
+ console.warn('Failed to get usage data:', _usageErr);
62
+ }
63
+
64
+ return { usageData, structuredOutputData };
65
+ }
@@ -0,0 +1,82 @@
1
+ import { Output, jsonSchema } from 'ai';
2
+ import { findModel, findModelVariant, getLLMTemperature, getLLMMaxSteps, getModelsConfig } from '../../configuration/runtime';
3
+ import { getProvider } from '../../providers/registry';
4
+ import { buildSchemaPromptInstruction } from '../structured-output';
5
+ import type { ResponseFormat } from '@capekai/types';
6
+
7
+ export interface StreamConfigResult {
8
+ providerOptions: Record<string, Record<string, unknown>> | undefined;
9
+ usePromptBasedStructuredOutput: boolean;
10
+ streamOutput: ReturnType<typeof Output.object> | undefined;
11
+ temperature: number;
12
+ maxSteps: number;
13
+ /** System message with structured output instruction appended (if prompt-based mode) */
14
+ systemMessage: string;
15
+ }
16
+
17
+ export interface StreamConfigOptions {
18
+ modelId: string | undefined;
19
+ providerId: string | undefined;
20
+ variant: string | undefined;
21
+ systemMessage: string;
22
+ baseProviderOptions: Record<string, Record<string, unknown>> | undefined;
23
+ responseFormat?: ResponseFormat;
24
+ temperature?: number;
25
+ maxSteps?: number;
26
+ }
27
+
28
+ export function buildStreamConfig(options: StreamConfigOptions): StreamConfigResult {
29
+ const {
30
+ modelId,
31
+ providerId,
32
+ variant,
33
+ systemMessage,
34
+ baseProviderOptions,
35
+ responseFormat,
36
+ temperature,
37
+ maxSteps,
38
+ } = options;
39
+
40
+ // Resolve variant providerOptions
41
+ const variantOpts = variant ? findModelVariant(modelId || '', variant) : undefined;
42
+
43
+ // Determine the provider-specific providerOptions key
44
+ const resolvedProvider = providerId || getModelsConfig().defaultProvider;
45
+ const registered = resolvedProvider ? getProvider(resolvedProvider) : undefined;
46
+ const providerOptionsKey = registered?.descriptor.providerOptionsKey ?? resolvedProvider;
47
+
48
+ // Build merged providerOptions
49
+ let providerOptions: Record<string, Record<string, unknown>> | undefined;
50
+ if (baseProviderOptions) {
51
+ providerOptions = {
52
+ ...baseProviderOptions,
53
+ ...(variantOpts ? { [providerOptionsKey]: { ...(baseProviderOptions[providerOptionsKey] || {}), ...variantOpts } } : {}),
54
+ };
55
+ } else if (variantOpts) {
56
+ providerOptions = { [providerOptionsKey]: variantOpts };
57
+ }
58
+
59
+ // Structured output handling
60
+ const modelDef = modelId ? findModel(modelId) : undefined;
61
+ const structuredOutputMode = modelDef?.capabilities?.structuredOutput?.mode ?? 'native';
62
+ const usePromptBasedStructuredOutput: boolean =
63
+ !!(responseFormat && structuredOutputMode === 'prompt');
64
+
65
+ let finalSystemMessage = systemMessage;
66
+ if (usePromptBasedStructuredOutput && responseFormat && systemMessage) {
67
+ finalSystemMessage = systemMessage + '\n\n' + buildSchemaPromptInstruction(responseFormat);
68
+ }
69
+
70
+ const streamOutput = responseFormat && !usePromptBasedStructuredOutput
71
+ ? Output.object({ schema: jsonSchema(responseFormat.schema) })
72
+ : undefined;
73
+
74
+ return {
75
+ providerOptions,
76
+ usePromptBasedStructuredOutput,
77
+ streamOutput,
78
+ temperature: temperature ?? getLLMTemperature(),
79
+ maxSteps: maxSteps ?? getLLMMaxSteps(),
80
+ systemMessage: finalSystemMessage,
81
+ };
82
+ }