@beeuni/sdk 0.1.0 → 0.2.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 (106) hide show
  1. package/README.md +35 -83
  2. package/dist/beeuni.esm.min.js +35 -35
  3. package/dist/beeuni.global.js +35 -35
  4. package/dist/browser/stores/sync-store.d.ts +6 -0
  5. package/dist/browser/stores/sync-store.js +52 -21
  6. package/dist/core/client/beeuni.d.ts +7 -29
  7. package/dist/core/client/beeuni.js +3 -11
  8. package/dist/core/client/facade-catalog.d.ts +0 -4
  9. package/dist/core/client/facade-catalog.js +1 -5
  10. package/dist/core/client/project-connections.d.ts +2 -2
  11. package/dist/core/client/project-connections.js +2 -2
  12. package/dist/core/client/project-operations.d.ts +4 -15
  13. package/dist/core/client/project-operations.js +2 -6
  14. package/dist/core/client/project-platform-security.d.ts +1 -1
  15. package/dist/core/client/project-platform-settings.d.ts +1 -1
  16. package/dist/core/http/api-client.d.ts +0 -7
  17. package/dist/core/http/api-client.js +1 -37
  18. package/dist/core/http/auth.d.ts +2 -4
  19. package/dist/core/http/auth.js +2 -8
  20. package/dist/core/rest/platform-client/client-policy.d.ts +8 -0
  21. package/dist/core/rest/platform-client/client-policy.js +30 -0
  22. package/dist/core/rest/platform-client/index.d.ts +3 -4
  23. package/dist/core/rest/platform-client/index.js +3 -4
  24. package/dist/core/rest/platform-client/types.d.ts +1 -1
  25. package/dist/core/rest/projects-client/apps.d.ts +8 -1
  26. package/dist/core/rest/projects-client/connectors.d.ts +6 -55
  27. package/dist/core/rest/projects-client/connectors.js +2 -29
  28. package/dist/core/rest/projects-client/git-backend.d.ts +1 -10
  29. package/dist/core/rest/projects-client/git-backend.js +0 -10
  30. package/dist/core/rest/projects-client/iam-sso.d.ts +0 -10
  31. package/dist/core/rest/projects-client/iam-sso.js +0 -3
  32. package/dist/core/rest/projects-client/index.d.ts +1 -4
  33. package/dist/core/rest/projects-client/index.js +1 -4
  34. package/dist/core/rest/projects-client/projects.d.ts +2 -1
  35. package/dist/core/rest/projects-client/projects.js +3 -0
  36. package/dist/core/rest/projects-client/sandbox.d.ts +8 -9
  37. package/dist/core/rest/projects-client/session-sandbox.d.ts +1 -1
  38. package/dist/core/rest/projects-client/sessions.d.ts +2 -2
  39. package/dist/core/session/control-stream.d.ts +1 -0
  40. package/dist/core/session/control-stream.js +18 -1
  41. package/dist/core/session-log/guard.d.ts +6 -0
  42. package/dist/core/session-log/guard.js +109 -0
  43. package/dist/core/session-log/index.d.ts +3 -0
  44. package/dist/core/session-log/index.js +2 -0
  45. package/dist/core/session-log/saved-log.d.ts +10 -0
  46. package/dist/core/session-log/saved-log.js +25 -0
  47. package/dist/core/session-log/source.d.ts +25 -0
  48. package/dist/core/session-log/source.js +30 -0
  49. package/dist/core/session-log/store.d.ts +21 -0
  50. package/dist/core/session-log/store.js +126 -0
  51. package/dist/core/session-log/tool-call.d.ts +6 -0
  52. package/dist/core/session-log/tool-call.js +46 -0
  53. package/dist/core/session-log/types.d.ts +264 -0
  54. package/dist/core/session-log/types.js +6 -0
  55. package/dist/core/session-log/v1-view.d.ts +10 -0
  56. package/dist/core/session-log/v1-view.js +151 -0
  57. package/dist/core/session-log/v1.d.ts +31 -0
  58. package/dist/core/session-log/v1.js +466 -0
  59. package/dist/core/session-sync/copy-store-types.d.ts +19 -0
  60. package/dist/core/session-sync/copy-store-types.js +1 -0
  61. package/dist/core/session-sync/saved-copy-store.js +25 -4
  62. package/dist/core/stream/event-stream.js +2 -2
  63. package/dist/core/turns/classify.d.ts +2 -1
  64. package/dist/core/turns/classify.js +3 -1
  65. package/dist/core/turns/parts.d.ts +3 -2
  66. package/dist/core/turns/parts.js +4 -2
  67. package/dist/core/turns/segments/index.d.ts +1 -0
  68. package/dist/core/turns/segments/index.js +1 -0
  69. package/dist/core/turns/segments/work-split.d.ts +10 -0
  70. package/dist/core/turns/segments/work-split.js +40 -0
  71. package/dist/core/turns/state.d.ts +2 -7
  72. package/dist/core/turns/state.js +28 -134
  73. package/dist/core/turns/tools/tool-meta.d.ts +2 -1
  74. package/dist/core/turns/tools/tool-meta.js +3 -1
  75. package/dist/core/turns/tools/tool-outcome.d.ts +2 -1
  76. package/dist/core/turns/tools/tool-outcome.js +3 -1
  77. package/dist/core/turns/types.d.ts +1 -1
  78. package/dist/core/warm-session-pool.d.ts +29 -0
  79. package/dist/core/warm-session-pool.js +127 -0
  80. package/dist/index.d.ts +2 -2
  81. package/dist/index.js +2 -2
  82. package/dist/internal/connector-catalog.d.ts +47 -0
  83. package/dist/internal/connector-catalog.js +27 -0
  84. package/dist/internal/host-configuration.d.ts +22 -0
  85. package/dist/internal/host-configuration.js +28 -0
  86. package/dist/internal/platform-admin-react.d.ts +12 -0
  87. package/dist/internal/platform-admin-react.js +13 -0
  88. package/dist/internal/platform-admin.d.ts +9 -0
  89. package/dist/internal/platform-admin.js +9 -0
  90. package/dist/react/index.d.ts +0 -13
  91. package/dist/react/index.js +0 -13
  92. package/dist/react/opencode.d.ts +1 -0
  93. package/dist/react/opencode.js +1 -0
  94. package/dist/react/query-keys.d.ts +0 -1
  95. package/dist/react/query-keys.js +0 -1
  96. package/dist/react/session-transcript-subscription.js +3 -1
  97. package/dist/react/sign-in-with-beeuni.d.ts +1 -1
  98. package/dist/react/use-admin-providers.d.ts +5 -5
  99. package/dist/react/use-opencode-events/handle-event.d.ts +1 -0
  100. package/dist/react/use-opencode-events/handle-event.js +5 -0
  101. package/dist/react/use-opencode-events/index.js +1 -0
  102. package/dist/react/use-runtime-reconnect.js +2 -2
  103. package/dist/react/use-session-log.d.ts +15 -0
  104. package/dist/react/use-session-log.js +134 -0
  105. package/dist/react/use-session.js +2 -0
  106. package/package.json +15 -7
@@ -1,3 +1,4 @@
1
+ import { registerSessionLogCopies, SESSION_LOG_COPY } from '../session-log/saved-log.js';
1
2
  export function isPaintableSavedCopy(envelope) {
2
3
  return (!!envelope &&
3
4
  envelope.available === true &&
@@ -58,7 +59,27 @@ function capturedAtMs(envelope) {
58
59
  const parsed = envelope.captured_at ? Date.parse(envelope.captured_at) : Number.NaN;
59
60
  return Number.isFinite(parsed) ? parsed : 0;
60
61
  }
62
+ const ENVELOPE = {
63
+ paintable: isPaintableSavedCopy,
64
+ fit: fitSavedCopy,
65
+ capturedAt: capturedAtMs,
66
+ };
61
67
  export function createSavedCopyStore(options) {
68
+ const v1 = createCopyStore(options, ENVELOPE);
69
+ const v2 = createCopyStore({ ...options, namespace: `${options.namespace ?? DEFAULT_NAMESPACE}.log` }, SESSION_LOG_COPY);
70
+ const store = {
71
+ ...v1,
72
+ remove: async (projectId, sessionId) => {
73
+ await Promise.all([v1.remove(projectId, sessionId), v2.remove(projectId, sessionId)]);
74
+ },
75
+ clear: async () => {
76
+ await Promise.all([v1.clear(), v2.clear()]);
77
+ },
78
+ };
79
+ registerSessionLogCopies(store, v2, v1.remove);
80
+ return store;
81
+ }
82
+ function createCopyStore(options, codec) {
62
83
  const { storage, userId, maxSessions = DEFAULT_MAX_SESSIONS, maxBytes = DEFAULT_MAX_BYTES, maxEnvelopeBytes = DEFAULT_MAX_ENVELOPE_BYTES, maxAgeMs = DEFAULT_MAX_AGE_MS, namespace = DEFAULT_NAMESPACE, now = () => Date.now(), } = options;
63
84
  const prefix = `${namespace}:${userId}:`;
64
85
  const indexKey = `${prefix}index`;
@@ -163,7 +184,7 @@ export function createSavedCopyStore(options) {
163
184
  stored.v !== VERSION ||
164
185
  stored.user !== userId ||
165
186
  now() - stored.at > maxAgeMs ||
166
- !isPaintableSavedCopy(stored.e)) {
187
+ !codec.paintable(stored.e)) {
167
188
  removeNow(keyFor(projectId, sessionId));
168
189
  void forgetScope(scope);
169
190
  return null;
@@ -174,13 +195,13 @@ export function createSavedCopyStore(options) {
174
195
  },
175
196
  write(projectId, sessionId, envelope) {
176
197
  const scope = `${projectId}/${sessionId}`;
177
- if (!isPaintableSavedCopy(envelope))
198
+ if (!codec.paintable(envelope))
178
199
  return forgetScope(scope);
179
200
  const stored = { v: VERSION, user: userId, at: now(), e: envelope };
180
- const text = fitSavedCopy(stored, maxEnvelopeBytes);
201
+ const text = codec.fit(stored, maxEnvelopeBytes);
181
202
  if (text === null)
182
203
  return forgetScope(scope);
183
- const captured = capturedAtMs(envelope);
204
+ const captured = codec.capturedAt(envelope);
184
205
  return enqueue(async () => {
185
206
  const entries = await loadIndex();
186
207
  const existing = entries.find((entry) => entry.s === scope);
@@ -1,4 +1,4 @@
1
- import { getSupabaseAccessToken, invalidateTokenCache } from '../http/auth.js';
1
+ import { getAuthToken, invalidateTokenCache } from '../http/auth.js';
2
2
  import { getClientForUrl } from '../runtime/client.js';
3
3
  import { isAuthFailure } from '../http/api/errors.js';
4
4
  import { logger } from '../http/logger.js';
@@ -241,7 +241,7 @@ function createLiveStream(client, opts, subscribers) {
241
241
  if (isAuthError) {
242
242
  try {
243
243
  invalidateTokenCache();
244
- await getSupabaseAccessToken();
244
+ await getAuthToken();
245
245
  logger.info('SSE: refreshed auth token after auth error');
246
246
  }
247
247
  catch (refreshErr) {
@@ -1,6 +1,7 @@
1
1
  import type { MessageWithParts } from '../../transcript.js';
2
2
  import type { Part, ToolPart } from '../runtime/client.js';
3
3
  import { type GatewayAttemptFailure } from './errors.js';
4
+ import type { ToolCallBlock } from '../session-log/types.js';
4
5
  import { type ToolKind } from './tool-kind.js';
5
6
  import type { TokenUsageLike } from './types.js';
6
7
  export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
@@ -30,7 +31,7 @@ export interface ToolView {
30
31
  answers?: string[][];
31
32
  childSessionId?: string;
32
33
  }
33
- export declare function toToolView(part: Pick<ToolPart, 'tool' | 'state'>): ToolView;
34
+ export declare function toToolView(call: Pick<ToolPart, 'tool' | 'state'> | ToolCallBlock): ToolView;
34
35
  export interface ClassifiedTextPart {
35
36
  kind: 'text';
36
37
  id: string;
@@ -1,5 +1,6 @@
1
1
  import { extractGatewayErrorDetails, unwrapError } from './errors.js';
2
2
  import { getChildSessionId } from './parts.js';
3
+ import { isToolCallBlock, toolPartOf } from '../session-log/tool-call.js';
3
4
  import { inputPath, toolKind } from './tool-kind.js';
4
5
  import { toolInfo } from './tool-registry.js';
5
6
  const MAX_PARSEABLE_OUTPUT_LENGTH = 256 * 1024;
@@ -153,7 +154,8 @@ function classifyToolState(tool, state) {
153
154
  childSessionId: getChildSessionId({ tool, state }),
154
155
  };
155
156
  }
156
- export function toToolView(part) {
157
+ export function toToolView(call) {
158
+ const part = isToolCallBlock(call) ? toolPartOf(call) : call;
157
159
  return classifyToolState(part.tool, part.state);
158
160
  }
159
161
  function classifyText(part) {
@@ -1,3 +1,4 @@
1
+ import type { ToolCallBlock } from '../session-log/types.js';
1
2
  import type { Diagnostic, MessageWithPartsLike, PartLike, RequestWithToolLike, ToolInfo, ToolPartLike } from './types.js';
2
3
  export declare function isTextPart<P extends PartLike>(part: P): part is P & {
3
4
  type: 'text';
@@ -42,8 +43,8 @@ export interface HiddenToolRef {
42
43
  }
43
44
  export declare function getHiddenToolParts(permission: RequestWithToolLike | undefined, question: RequestWithToolLike | undefined): HiddenToolRef[];
44
45
  export declare function isToolPartHidden(part: Pick<ToolPartLike, 'callID'>, messageId: string, hidden: HiddenToolRef[]): boolean;
45
- export declare function shouldShowToolPart(part: Pick<ToolPartLike, 'tool'>): boolean;
46
- export declare function getChildSessionId(part: Pick<ToolPartLike, 'tool' | 'state'>): string | undefined;
46
+ export declare function shouldShowToolPart(part: Pick<ToolPartLike, 'tool'> | ToolCallBlock): boolean;
47
+ export declare function getChildSessionId(call: Pick<ToolPartLike, 'tool' | 'state'> | ToolCallBlock): string | undefined;
47
48
  export declare function getChildSessionError(childMessages: readonly MessageWithPartsLike[] | undefined): string | undefined;
48
49
  export declare function getChildSessionToolParts<M extends MessageWithPartsLike>(childMessages: readonly M[]): Array<M['parts'][number] & {
49
50
  type: 'tool';
@@ -1,4 +1,5 @@
1
1
  import { unwrapError } from './errors.js';
2
+ import { isToolCallBlock, toolPartOf } from '../session-log/tool-call.js';
2
3
  import { inputPath, toolKind } from './tool-kind.js';
3
4
  import { normalizeName } from './tools/tool-meta.js';
4
5
  export function isTextPart(part) {
@@ -67,7 +68,7 @@ export function isToolPartHidden(part, messageId, hidden) {
67
68
  }
68
69
  const HIDDEN_TOOLS = new Set(['todoread', 'context_info']);
69
70
  export function shouldShowToolPart(part) {
70
- return !HIDDEN_TOOLS.has(part.tool);
71
+ return !HIDDEN_TOOLS.has(isToolCallBlock(part) ? part.name : part.tool);
71
72
  }
72
73
  const CHILD_SESSION_TOOLS = new Set([
73
74
  'task',
@@ -82,7 +83,8 @@ const CHILD_SESSION_TOOLS = new Set([
82
83
  'task_update',
83
84
  'task_message',
84
85
  ]);
85
- export function getChildSessionId(part) {
86
+ export function getChildSessionId(call) {
87
+ const part = isToolCallBlock(call) ? toolPartOf(call) : call;
86
88
  const tool = part.tool || '';
87
89
  if (tool.startsWith('oc-'))
88
90
  return undefined;
@@ -10,3 +10,4 @@ export * from './show-group.js';
10
10
  export * from './step-label.js';
11
11
  export * from './working-turn.js';
12
12
  export * from './turn-busy-visibility.js';
13
+ export * from './work-split.js';
@@ -10,3 +10,4 @@ export * from './show-group.js';
10
10
  export * from './step-label.js';
11
11
  export * from './working-turn.js';
12
12
  export * from './turn-busy-visibility.js';
13
+ export * from './work-split.js';
@@ -0,0 +1,10 @@
1
+ import type { ShowGroupedSegment } from './show-group.js';
2
+ export interface TurnWorkSplit<S extends ShowGroupedSegment = ShowGroupedSegment> {
3
+ work: S[];
4
+ hoisted: S[];
5
+ final: S[];
6
+ }
7
+ export declare function splitTurnWork<S extends ShowGroupedSegment = ShowGroupedSegment>(segments: ReadonlyArray<S>, opts: {
8
+ working: boolean;
9
+ }): TurnWorkSplit<S> | null;
10
+ export declare function workedForLabel(durationMs: number | null): string;
@@ -0,0 +1,40 @@
1
+ import { formatDuration } from '../state.js';
2
+ import { normalizeActivityToolName } from './session-activity-groups.js';
3
+ function isDeliverableSegment(segment) {
4
+ if (segment.kind === 'show-group')
5
+ return true;
6
+ if (segment.kind !== 'standalone')
7
+ return false;
8
+ const name = normalizeActivityToolName(segment.part.tool);
9
+ return name === 'show' || name === 'show_user';
10
+ }
11
+ function isBlankText(segment) {
12
+ return segment.kind === 'text' && !segment.part.text?.trim();
13
+ }
14
+ export function splitTurnWork(segments, opts) {
15
+ if (opts.working)
16
+ return null;
17
+ let finalIndex = -1;
18
+ for (let i = segments.length - 1; i >= 0; i--) {
19
+ const segment = segments[i];
20
+ if (isDeliverableSegment(segment) || isBlankText(segment))
21
+ continue;
22
+ if (segment.kind === 'text')
23
+ finalIndex = i;
24
+ break;
25
+ }
26
+ const split = { work: [], hoisted: [], final: [] };
27
+ segments.forEach((segment, i) => {
28
+ if (i === finalIndex)
29
+ split.final.push(segment);
30
+ else if (isDeliverableSegment(segment))
31
+ split.hoisted.push(segment);
32
+ else
33
+ split.work.push(segment);
34
+ });
35
+ return split.work.length > 0 ? split : null;
36
+ }
37
+ export function workedForLabel(durationMs) {
38
+ const duration = durationMs == null ? '' : formatDuration(durationMs);
39
+ return duration ? `Worked for ${duration}` : 'Worked';
40
+ }
@@ -1,5 +1,5 @@
1
1
  import { extractGatewayErrorDetails } from './errors.js';
2
- import type { MessageInfoLike, MessageWithPartsLike, ModelPricingLookup, PartLike, PartWithMessage, RetryInfo, SessionStatusLike, TurnCostInfo, TurnLike } from './types.js';
2
+ import type { MessageWithPartsLike, PartLike, PartWithMessage, RetryInfo, SessionStatusLike, TurnCostInfo, TurnLike } from './types.js';
3
3
  export declare function isLastUserMessage(messageId: string, allMessages: readonly MessageWithPartsLike[]): boolean;
4
4
  export declare function getWorkingState(sessionStatus: SessionStatusLike | undefined, isLast: boolean): boolean;
5
5
  export declare function shouldHideResponsePart(working: boolean, responsePartId: string | undefined): boolean;
@@ -11,12 +11,7 @@ export declare function getTurnStatus(parts: ReadonlyArray<{
11
11
  part: PartLike;
12
12
  }>, childMessages?: readonly MessageWithPartsLike[]): string;
13
13
  export declare function formatDuration(ms: number): string;
14
- export declare const COST_MARKUP = 1.2;
15
- export declare function getTurnCost(parts: ReadonlyArray<PartWithMessage>, lookup?: ModelPricingLookup): TurnCostInfo | undefined;
16
- export declare function getSessionCost(messages: ReadonlyArray<{
17
- info?: MessageInfoLike;
18
- parts: PartLike[];
19
- }>, lookup?: ModelPricingLookup): number;
14
+ export declare function getTurnTokens(parts: ReadonlyArray<PartWithMessage>): TurnCostInfo['tokens'] | undefined;
20
15
  export declare function formatCost(cost: number): string;
21
16
  export declare function formatTokens(tokens: number): string;
22
17
  export declare function getRetryInfo(status: SessionStatusLike | undefined): RetryInfo | undefined;
@@ -185,145 +185,39 @@ export function formatDuration(ms) {
185
185
  const secs = seconds % 60;
186
186
  return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`;
187
187
  }
188
- export const COST_MARKUP = 1.2;
189
- function estimateTokenCost(tokens, rates) {
190
- if (!tokens)
191
- return 0;
192
- const input = tokens.input ?? 0;
193
- const output = (tokens.output ?? 0) + (tokens.reasoning ?? 0);
194
- const cacheRead = tokens.cache?.read ?? 0;
195
- const cacheWrite = tokens.cache?.write ?? 0;
196
- const regularInput = Math.max(0, input - cacheRead - cacheWrite);
197
- const tierCandidates = [
198
- ...(rates.tiers ?? []),
199
- ...(rates.contextOver200k ? [rates.contextOver200k] : []),
200
- ]
201
- .filter((tier) => input > tier.contextThreshold)
202
- .sort((a, b) => b.contextThreshold - a.contextThreshold);
203
- const effective = tierCandidates[0] ?? rates;
204
- let cost = (regularInput / 1_000_000) * effective.inputPer1M;
205
- cost += (output / 1_000_000) * effective.outputPer1M;
206
- if (cacheRead > 0) {
207
- cost += (cacheRead / 1_000_000) * (effective.cacheReadPer1M ?? effective.inputPer1M);
208
- }
209
- if (cacheWrite > 0) {
210
- cost += (cacheWrite / 1_000_000) * (effective.cacheWritePer1M ?? effective.inputPer1M);
211
- }
212
- return cost;
213
- }
214
- function isSubscriptionUsage(providerID, modelID) {
215
- return (providerID === 'codex' ||
216
- providerID === 'openai-codex' ||
217
- (providerID === 'beeuni' && !!modelID?.startsWith('codex/')));
218
- }
219
- function stepFinishRawCost(sfp, providerID, modelID, lookup) {
220
- if (isSubscriptionUsage(providerID, modelID))
221
- return 0;
222
- const reported = sfp.cost || 0;
223
- if (reported > 0)
224
- return reported;
225
- if (!lookup || !providerID || !modelID)
226
- return 0;
227
- const rates = lookup(providerID, modelID);
228
- if (!rates)
229
- return 0;
230
- return estimateTokenCost(sfp.tokens, rates);
231
- }
232
- function assistantMessageIds(message) {
233
- if (message.role !== 'assistant')
234
- return {};
235
- const assistant = message;
236
- return { providerID: assistant.providerID, modelID: assistant.modelID };
237
- }
238
- export function getTurnCost(parts, lookup) {
239
- let totalCost = 0;
240
- let input = 0;
241
- let output = 0;
242
- let reasoning = 0;
243
- let cacheRead = 0;
244
- let cacheWrite = 0;
188
+ export function getTurnTokens(parts) {
189
+ const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 };
245
190
  let found = false;
191
+ const add = (usage) => {
192
+ found = true;
193
+ tokens.input += usage.input ?? 0;
194
+ tokens.output += usage.output ?? 0;
195
+ tokens.reasoning += usage.reasoning ?? 0;
196
+ tokens.cacheRead += usage.cache?.read ?? 0;
197
+ tokens.cacheWrite += usage.cache?.write ?? 0;
198
+ };
199
+ const stepMessages = new Set();
246
200
  for (const { part, message } of parts) {
247
- if (part.type === 'step-finish') {
248
- found = true;
249
- const sfp = part;
250
- const { providerID, modelID } = assistantMessageIds(message.info);
251
- totalCost += stepFinishRawCost(sfp, providerID, modelID, lookup);
252
- input += sfp.tokens?.input || 0;
253
- output += sfp.tokens?.output || 0;
254
- reasoning += sfp.tokens?.reasoning || 0;
255
- cacheRead += sfp.tokens?.cache?.read || 0;
256
- cacheWrite += sfp.tokens?.cache?.write || 0;
257
- }
258
- }
259
- if (!found) {
260
- const seen = new Set();
261
- for (const { message } of parts) {
262
- if (message.info.role !== 'assistant')
263
- continue;
264
- const assistant = message.info;
265
- if (seen.has(assistant.id))
266
- continue;
267
- seen.add(assistant.id);
268
- const stepCost = assistantTokensCost(assistant, lookup);
269
- const subscriptionTokens = assistant.tokens && isSubscriptionUsage(assistant.providerID, assistant.modelID);
270
- if (stepCost <= 0 && !subscriptionTokens)
271
- continue;
272
- found = true;
273
- totalCost += stepCost;
274
- const t = assistant.tokens;
275
- if (t) {
276
- input += t.input ?? 0;
277
- output += t.output ?? 0;
278
- reasoning += t.reasoning ?? 0;
279
- cacheRead += t.cache?.read ?? 0;
280
- cacheWrite += t.cache?.write ?? 0;
281
- }
282
- }
201
+ if (part.type !== 'step-finish')
202
+ continue;
203
+ const usage = part.tokens;
204
+ if (!usage)
205
+ continue;
206
+ stepMessages.add(message.info.id);
207
+ add(usage);
283
208
  }
284
- if (!found)
285
- return undefined;
286
- return {
287
- cost: totalCost * COST_MARKUP,
288
- tokens: { input, output, reasoning, cacheRead, cacheWrite },
289
- };
290
- }
291
- function assistantTokensCost(assistant, lookup) {
292
- if (isSubscriptionUsage(assistant.providerID, assistant.modelID))
293
- return 0;
294
- if (!lookup || !assistant.providerID || !assistant.modelID || !assistant.tokens)
295
- return 0;
296
- const rates = lookup(assistant.providerID, assistant.modelID);
297
- if (!rates)
298
- return 0;
299
- return estimateTokenCost({
300
- input: assistant.tokens.input,
301
- output: assistant.tokens.output,
302
- reasoning: assistant.tokens.reasoning,
303
- cache: assistant.tokens.cache,
304
- }, rates);
305
- }
306
- export function getSessionCost(messages, lookup) {
307
- let totalCost = 0;
308
- for (const msg of messages) {
309
- if (msg.info?.role !== 'assistant')
209
+ const seen = new Set();
210
+ for (const { message } of parts) {
211
+ if (message.info.role !== 'assistant' ||
212
+ stepMessages.has(message.info.id) ||
213
+ seen.has(message.info.id))
310
214
  continue;
311
- const assistant = msg.info;
312
- const { providerID, modelID } = assistantMessageIds(assistant);
313
- let msgCost = 0;
314
- let sawStepFinish = false;
315
- for (const part of msg.parts) {
316
- if (part.type !== 'step-finish')
317
- continue;
318
- sawStepFinish = true;
319
- msgCost += stepFinishRawCost(part, providerID, modelID, lookup);
320
- }
321
- if (!sawStepFinish) {
322
- msgCost += assistantTokensCost(assistant, lookup);
323
- }
324
- totalCost += msgCost;
215
+ seen.add(message.info.id);
216
+ const usage = message.info.tokens;
217
+ if (usage)
218
+ add(usage);
325
219
  }
326
- return totalCost * COST_MARKUP;
220
+ return found ? tokens : undefined;
327
221
  }
328
222
  export function formatCost(cost) {
329
223
  if (cost === 0)
@@ -1,4 +1,5 @@
1
1
  import type { ToolPart } from '../../runtime/client.js';
2
+ import type { ToolCallBlock } from '../../session-log/types.js';
2
3
  export declare const CONTEXT_TOOLS: Set<string>;
3
4
  export declare function normalizeName(name: string): string;
4
5
  export declare function isContextTool(toolName: string): boolean;
@@ -24,7 +25,7 @@ export interface ContextToolTriggerLabels {
24
25
  workspace: string;
25
26
  }
26
27
  export declare const DEFAULT_CONTEXT_TOOL_TRIGGER_LABELS: Readonly<ContextToolTriggerLabels>;
27
- export declare function contextToolTrigger(part: ToolPart, labels?: Partial<ContextToolTriggerLabels>): {
28
+ export declare function contextToolTrigger(call: ToolPart | ToolCallBlock, labels?: Partial<ContextToolTriggerLabels>): {
28
29
  title: string;
29
30
  subtitle: string;
30
31
  };
@@ -1,4 +1,5 @@
1
1
  import { toolKind } from '../tool-kind.js';
2
+ import { isToolCallBlock, toolPartOf } from '../../session-log/tool-call.js';
2
3
  import { humanizeSearchQuery } from './search-query.js';
3
4
  import { stripTrailingSlashes } from '../text-scan.js';
4
5
  export const CONTEXT_TOOLS = new Set(['read', 'glob', 'grep', 'list']);
@@ -100,7 +101,8 @@ export const DEFAULT_CONTEXT_TOOL_TRIGGER_LABELS = {
100
101
  worker: 'Worker',
101
102
  workspace: 'Workspace',
102
103
  };
103
- export function contextToolTrigger(part, labels = {}) {
104
+ export function contextToolTrigger(call, labels = {}) {
105
+ const part = isToolCallBlock(call) ? toolPartOf(call) : call;
104
106
  const l = { ...DEFAULT_CONTEXT_TOOL_TRIGGER_LABELS, ...labels };
105
107
  const n = normalizeName(part.tool);
106
108
  const sub = getToolPrimaryArg(part);
@@ -1,4 +1,5 @@
1
1
  import type { ToolPart } from '../../runtime/client.js';
2
+ import type { ToolCallBlock } from '../../session-log/types.js';
2
3
  export interface ParsedJsonFailure {
3
4
  errorSummary: string;
4
5
  hint?: string;
@@ -9,7 +10,7 @@ export interface ParsedJsonFailure {
9
10
  export type ToolOutcome = 'ok' | 'partial' | 'failed';
10
11
  export declare function looksLikeError(text: string): boolean;
11
12
  export declare function isErrorOutput(output: string): boolean;
12
- export declare function partOutcome(part: Pick<ToolPart, 'state'>): ToolOutcome;
13
+ export declare function partOutcome(call: Pick<ToolPart, 'state'> | ToolCallBlock): ToolOutcome;
13
14
  export declare function parseJsonFailure(output: string): ParsedJsonFailure | null;
14
15
  export declare function cleanErrorMessage(raw: string): string;
15
16
  export declare function formatJsonFailureOutput(output: string): string | null;
@@ -1,4 +1,5 @@
1
1
  import { jsonTail, stripErrorPrefixes } from '../text-scan.js';
2
+ import { isToolCallBlock, toolPartOf } from '../../session-log/tool-call.js';
2
3
  function isJsonObject(value) {
3
4
  return value !== null && typeof value === 'object' && !Array.isArray(value);
4
5
  }
@@ -19,7 +20,8 @@ export function looksLikeError(text) {
19
20
  export function isErrorOutput(output) {
20
21
  return !!output && (looksLikeError(output) || parseJsonFailure(output) !== null);
21
22
  }
22
- export function partOutcome(part) {
23
+ export function partOutcome(call) {
24
+ const part = isToolCallBlock(call) ? toolPartOf(call) : call;
23
25
  const cached = OUTCOME_CACHE.get(part);
24
26
  if (cached && cached.state === part.state)
25
27
  return cached.outcome;
@@ -79,7 +79,7 @@ export interface ToolInfo {
79
79
  subtitle?: string;
80
80
  }
81
81
  export interface TurnCostInfo {
82
- cost: number;
82
+ cost: number | null;
83
83
  tokens: {
84
84
  input: number;
85
85
  output: number;
@@ -0,0 +1,29 @@
1
+ export interface WarmPoolSession {
2
+ sessionId: string;
3
+ }
4
+ export interface WarmSessionPoolClient<Session extends WarmPoolSession, ClaimInput> {
5
+ ensure: (projectId: string, excludeSessionId?: string) => Promise<Session>;
6
+ claim: (projectId: string, input: ClaimInput) => Promise<unknown>;
7
+ read: (projectId: string, sessionId: string) => Promise<{
8
+ metadata?: unknown;
9
+ } | null>;
10
+ }
11
+ export interface WarmSessionPoolCoreOptions<Session extends WarmPoolSession, Send> {
12
+ fits: (session: Session, send: Send) => boolean;
13
+ sleep?: (ms: number) => Promise<void>;
14
+ }
15
+ export interface WarmSessionPoolCore<Session extends WarmPoolSession, Send, ClaimInput> {
16
+ ensure: (projectId: string, options?: {
17
+ excludeSessionId?: string;
18
+ }) => Promise<void>;
19
+ take: (projectId: string, send: Send, options: {
20
+ replenish: boolean;
21
+ }) => Session | null;
22
+ prime: (projectId: string, session: Session, input: ClaimInput) => Promise<boolean>;
23
+ dropBySessionId: (sessionId: string) => string | null;
24
+ revalidate: (projectId: string) => Promise<boolean>;
25
+ held: (projectId: string) => Session | null;
26
+ reset: () => void;
27
+ settled: () => Promise<void>;
28
+ }
29
+ export declare function createWarmSessionPool<Session extends WarmPoolSession, Send, ClaimInput>(client: WarmSessionPoolClient<Session, ClaimInput>, options: WarmSessionPoolCoreOptions<Session, Send>): WarmSessionPoolCore<Session, Send, ClaimInput>;
@@ -0,0 +1,127 @@
1
+ function isAmbiguousCreateFailure(code) {
2
+ return code === 'TIMEOUT' || code === 'request_deadline';
3
+ }
4
+ function errorCode(error) {
5
+ const code = isRecord(error) ? error.code : undefined;
6
+ return typeof code === 'string' ? code : undefined;
7
+ }
8
+ const isRecord = (value) => typeof value === 'object' && value !== null;
9
+ function isWarmRow(row) {
10
+ const metadata = row?.metadata;
11
+ return !!isRecord(metadata) && metadata.warm === true;
12
+ }
13
+ const COMMIT_CONFIRM_ATTEMPTS = 5;
14
+ const COMMIT_CONFIRM_DELAY_MS = 2_000;
15
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ async function confirmCommitted(probe, { attempts = COMMIT_CONFIRM_ATTEMPTS, delayMs = COMMIT_CONFIRM_DELAY_MS, sleep = wait, } = {}) {
17
+ for (let attempt = 0; attempt < attempts; attempt++) {
18
+ if (attempt > 0)
19
+ await sleep(delayMs);
20
+ try {
21
+ if (await probe())
22
+ return true;
23
+ }
24
+ catch {
25
+ }
26
+ }
27
+ return false;
28
+ }
29
+ export function createWarmSessionPool(client, options) {
30
+ const ready = new Map();
31
+ const creating = new Set();
32
+ const taken = new Set();
33
+ const inFlight = new Set();
34
+ let generation = 0;
35
+ const runEnsure = (projectId, excludeSessionId, retriesLeft) => {
36
+ if (creating.has(projectId) || ready.has(projectId))
37
+ return Promise.resolve();
38
+ creating.add(projectId);
39
+ const startedIn = generation;
40
+ const run = (async () => {
41
+ let retryExclude = null;
42
+ try {
43
+ const session = await client.ensure(projectId, excludeSessionId);
44
+ if (startedIn !== generation)
45
+ return;
46
+ if (taken.has(session.sessionId))
47
+ retryExclude = session.sessionId;
48
+ else
49
+ ready.set(projectId, session);
50
+ }
51
+ catch {
52
+ }
53
+ finally {
54
+ if (startedIn === generation)
55
+ creating.delete(projectId);
56
+ }
57
+ if (retryExclude && retriesLeft > 0) {
58
+ await runEnsure(projectId, retryExclude, retriesLeft - 1);
59
+ }
60
+ })();
61
+ inFlight.add(run);
62
+ void run.finally(() => inFlight.delete(run));
63
+ return run;
64
+ };
65
+ return {
66
+ ensure: (projectId, opts) => runEnsure(projectId, opts?.excludeSessionId, 1),
67
+ take: (projectId, send, { replenish }) => {
68
+ const warm = ready.get(projectId);
69
+ if (!warm)
70
+ return null;
71
+ ready.delete(projectId);
72
+ taken.add(warm.sessionId);
73
+ if (replenish)
74
+ void runEnsure(projectId, warm.sessionId, 1);
75
+ return options.fits(warm, send) ? warm : null;
76
+ },
77
+ prime: async (projectId, warm, input) => {
78
+ try {
79
+ await client.claim(projectId, input);
80
+ return true;
81
+ }
82
+ catch (error) {
83
+ if (!isAmbiguousCreateFailure(errorCode(error)))
84
+ return false;
85
+ return confirmCommitted(async () => !isWarmRow(await client.read(projectId, warm.sessionId)), options.sleep ? { sleep: options.sleep } : {});
86
+ }
87
+ },
88
+ dropBySessionId: (sessionId) => {
89
+ for (const [projectId, warm] of ready) {
90
+ if (warm.sessionId !== sessionId)
91
+ continue;
92
+ ready.delete(projectId);
93
+ taken.add(sessionId);
94
+ return projectId;
95
+ }
96
+ return null;
97
+ },
98
+ revalidate: async (projectId) => {
99
+ const held = ready.get(projectId);
100
+ if (!held)
101
+ return false;
102
+ try {
103
+ if (isWarmRow(await client.read(projectId, held.sessionId)))
104
+ return false;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ if (ready.get(projectId)?.sessionId !== held.sessionId)
110
+ return false;
111
+ ready.delete(projectId);
112
+ taken.add(held.sessionId);
113
+ return true;
114
+ },
115
+ held: (projectId) => ready.get(projectId) ?? null,
116
+ reset: () => {
117
+ generation += 1;
118
+ ready.clear();
119
+ creating.clear();
120
+ taken.clear();
121
+ },
122
+ settled: async () => {
123
+ while (inFlight.size > 0)
124
+ await Promise.all([...inFlight]);
125
+ },
126
+ };
127
+ }
package/dist/index.d.ts CHANGED
@@ -30,8 +30,6 @@ export * from './core/http/authenticated-request.js';
30
30
  export * from './core/http/config.js';
31
31
  export * from './core/http/feature-flags.js';
32
32
  export * from './core/http/fresh-sessions.js';
33
- export * from './core/http/impersonation.js';
34
- export * from './core/http/instance-routes.js';
35
33
  export * from './core/http/runtime-errors.js';
36
34
  export * from './core/rest/platform-client/index.js';
37
35
  export * from './core/rest/projects-client/index.js';
@@ -41,6 +39,8 @@ export * from './core/attachments/prompt-attachments.js';
41
39
  export * from './core/runtime/client.js';
42
40
  export * from './core/runtime/attachment-part.js';
43
41
  export * from './core/session/index.js';
42
+ export * from './core/session-log/index.js';
43
+ export * from './core/warm-session-pool.js';
44
44
  export { createHttpSessionSyncController, loadHttpSessionHistory, type SessionSyncMessage, } from './core/session-sync/session-sync-controller.js';
45
45
  export * from './core/session-sync/saved-copy-store.js';
46
46
  export { type EmptyConversationInput, isEmptyConversation, savedCopyEmptyRoot, } from './core/session-sync/saved-transcript.js';