typeclaw 0.3.0 → 0.3.1
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 +2 -1
- package/scripts/dump-system-prompt.ts +401 -0
- package/src/agent/index.ts +149 -30
- package/src/agent/provider-error.ts +44 -0
- package/src/agent/session-meta.ts +43 -0
- package/src/agent/subagents.ts +8 -0
- package/src/agent/system-prompt.ts +70 -35
- package/src/channels/router.ts +28 -2
- package/src/cli/usage.ts +30 -2
- package/src/config/config.ts +15 -4
- package/src/config/reloadable.ts +22 -4
- package/src/cron/consumer.ts +17 -1
- package/src/run/index.ts +9 -1
- package/src/server/index.ts +5 -10
- package/src/usage/aggregate.ts +30 -1
- package/src/usage/index.ts +3 -2
- package/src/usage/report.ts +103 -3
- package/src/usage/scan.ts +59 -4
package/src/usage/aggregate.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AssistantRow } from './scan'
|
|
1
|
+
import type { AssistantRow, OriginKind } from './scan'
|
|
2
|
+
import { ORIGIN_KINDS } from './scan'
|
|
2
3
|
|
|
3
4
|
export type UsageTotals = {
|
|
4
5
|
messageCount: number
|
|
@@ -18,13 +19,16 @@ export type SessionUsage = UsageTotals & {
|
|
|
18
19
|
firstAt: number
|
|
19
20
|
lastAt: number
|
|
20
21
|
models: string[]
|
|
22
|
+
originKind: OriginKind
|
|
21
23
|
}
|
|
24
|
+
export type OriginUsage = UsageTotals & { originKind: OriginKind; sessionCount: number }
|
|
22
25
|
|
|
23
26
|
export type Aggregation = {
|
|
24
27
|
total: UsageTotals
|
|
25
28
|
byDay: DailyUsage[]
|
|
26
29
|
byModel: ModelUsage[]
|
|
27
30
|
bySession: SessionUsage[]
|
|
31
|
+
byOrigin: OriginUsage[]
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export async function aggregate(rows: AsyncIterable<AssistantRow>): Promise<Aggregation> {
|
|
@@ -32,6 +36,7 @@ export async function aggregate(rows: AsyncIterable<AssistantRow>): Promise<Aggr
|
|
|
32
36
|
const byDay = new Map<string, DailyUsage & { _sessionIds: Set<string> }>()
|
|
33
37
|
const byModel = new Map<string, ModelUsage>()
|
|
34
38
|
const bySession = new Map<string, SessionUsage & { _modelSet: Set<string> }>()
|
|
39
|
+
const byOrigin = new Map<OriginKind, OriginUsage & { _sessionIds: Set<string> }>()
|
|
35
40
|
|
|
36
41
|
for await (const row of rows) {
|
|
37
42
|
addInto(total, row)
|
|
@@ -66,6 +71,7 @@ export async function aggregate(rows: AsyncIterable<AssistantRow>): Promise<Aggr
|
|
|
66
71
|
lastAt: row.timestamp,
|
|
67
72
|
models: [],
|
|
68
73
|
_modelSet: new Set<string>(),
|
|
74
|
+
originKind: row.originKind,
|
|
69
75
|
}
|
|
70
76
|
addInto(sessionBucket, row)
|
|
71
77
|
sessionBucket.firstAt = Math.min(sessionBucket.firstAt, row.timestamp)
|
|
@@ -73,6 +79,17 @@ export async function aggregate(rows: AsyncIterable<AssistantRow>): Promise<Aggr
|
|
|
73
79
|
sessionBucket._modelSet.add(modelKey)
|
|
74
80
|
sessionBucket.models = [...sessionBucket._modelSet]
|
|
75
81
|
bySession.set(sessionKey, sessionBucket)
|
|
82
|
+
|
|
83
|
+
const originBucket = byOrigin.get(row.originKind) ?? {
|
|
84
|
+
...emptyTotals(),
|
|
85
|
+
originKind: row.originKind,
|
|
86
|
+
sessionCount: 0,
|
|
87
|
+
_sessionIds: new Set<string>(),
|
|
88
|
+
}
|
|
89
|
+
addInto(originBucket, row)
|
|
90
|
+
originBucket._sessionIds.add(sessionKey)
|
|
91
|
+
originBucket.sessionCount = originBucket._sessionIds.size
|
|
92
|
+
byOrigin.set(row.originKind, originBucket)
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
return {
|
|
@@ -80,9 +97,21 @@ export async function aggregate(rows: AsyncIterable<AssistantRow>): Promise<Aggr
|
|
|
80
97
|
byDay: [...byDay.values()].map(({ _sessionIds: _, ...rest }) => rest).sort((a, b) => a.date.localeCompare(b.date)),
|
|
81
98
|
byModel: [...byModel.values()].sort((a, b) => b.cost - a.cost),
|
|
82
99
|
bySession: [...bySession.values()].map(({ _modelSet: _, ...rest }) => rest).sort((a, b) => b.cost - a.cost),
|
|
100
|
+
byOrigin: [...byOrigin.values()]
|
|
101
|
+
.map(({ _sessionIds: _, ...rest }) => rest)
|
|
102
|
+
.sort((a, b) => originSortIndex(a.originKind) - originSortIndex(b.originKind)),
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
105
|
|
|
106
|
+
// Stable presentation order for the byOrigin table. Matches ORIGIN_KINDS so
|
|
107
|
+
// the renderer doesn't need to know about ordering. 'unknown' is pinned last
|
|
108
|
+
// because it represents legacy/malformed data the user probably cares about
|
|
109
|
+
// least.
|
|
110
|
+
function originSortIndex(kind: OriginKind): number {
|
|
111
|
+
const idx = (ORIGIN_KINDS as readonly OriginKind[]).indexOf(kind)
|
|
112
|
+
return idx === -1 ? Number.MAX_SAFE_INTEGER : idx
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
function emptyTotals(): UsageTotals {
|
|
87
116
|
return { messageCount: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: 0 }
|
|
88
117
|
}
|
package/src/usage/index.ts
CHANGED
|
@@ -4,8 +4,9 @@ import type { Aggregation } from './aggregate'
|
|
|
4
4
|
import { aggregate } from './aggregate'
|
|
5
5
|
import { scanAssistantRows } from './scan'
|
|
6
6
|
|
|
7
|
-
export type { Aggregation, DailyUsage, ModelUsage, SessionUsage, UsageTotals } from './aggregate'
|
|
8
|
-
export type { AssistantRow } from './scan'
|
|
7
|
+
export type { Aggregation, DailyUsage, ModelUsage, OriginUsage, SessionUsage, UsageTotals } from './aggregate'
|
|
8
|
+
export type { AssistantRow, OriginKind } from './scan'
|
|
9
|
+
export { ORIGIN_KINDS } from './scan'
|
|
9
10
|
|
|
10
11
|
export type UsageReport = {
|
|
11
12
|
generatedAt: number
|
package/src/usage/report.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { styleText } from 'node:util'
|
|
2
2
|
|
|
3
|
-
import type { ModelUsage, UsageTotals } from './aggregate'
|
|
3
|
+
import type { ModelUsage, OriginUsage, UsageTotals } from './aggregate'
|
|
4
4
|
import { formatCacheHitRate, formatCost, formatTokens, isoDay } from './format'
|
|
5
5
|
import type { UsageReport } from './index'
|
|
6
|
+
import type { OriginKind } from './scan'
|
|
6
7
|
|
|
7
8
|
export type FormatOptions = {
|
|
8
9
|
useColor?: boolean
|
|
9
|
-
view?: 'summary' | 'daily' | 'session' | 'models'
|
|
10
|
+
view?: 'summary' | 'daily' | 'session' | 'models' | 'origin'
|
|
10
11
|
limit?: number
|
|
11
12
|
// Terminal width hint used to size the elastic Item column. Omit to render
|
|
12
13
|
// without truncation (tests, piped output where columns is undefined).
|
|
@@ -26,6 +27,8 @@ export function formatReport(report: UsageReport, opts: FormatOptions = {}): str
|
|
|
26
27
|
return renderSessions(report, ctx, opts.limit ?? 20)
|
|
27
28
|
case 'models':
|
|
28
29
|
return renderModels(report, ctx, opts.limit)
|
|
30
|
+
case 'origin':
|
|
31
|
+
return renderOrigin(report, ctx)
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -58,12 +61,20 @@ function renderSummary(report: UsageReport, ctx: RenderCtx): string {
|
|
|
58
61
|
|
|
59
62
|
if (aggregation.byDay.length > 0) {
|
|
60
63
|
sections.push('')
|
|
64
|
+
const trend = renderDailyTrend(aggregation.byDay, ctx)
|
|
65
|
+
if (trend !== null) sections.push(trend, '')
|
|
61
66
|
sections.push(header('By day (most recent first)', ctx))
|
|
62
67
|
const recent = aggregation.byDay.slice(-7).reverse()
|
|
63
68
|
const dayRows = recent.map((d) => ({ label: d.date, totals: d as UsageTotals }))
|
|
64
69
|
sections.push(renderTotalsTable(dayRows, ctx, { total: totalOfRows(dayRows) }))
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
if (aggregation.byOrigin.length > 0) {
|
|
73
|
+
sections.push('')
|
|
74
|
+
sections.push(header('By origin', ctx))
|
|
75
|
+
sections.push(renderOriginTable(aggregation.byOrigin, ctx))
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
if (aggregation.byModel.length > 0) {
|
|
68
79
|
sections.push('')
|
|
69
80
|
sections.push(header('By model', ctx))
|
|
@@ -84,6 +95,79 @@ function renderSummary(report: UsageReport, ctx: RenderCtx): string {
|
|
|
84
95
|
return sections.join('\n')
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
function renderOrigin(report: UsageReport, ctx: RenderCtx): string {
|
|
99
|
+
const sections: string[] = [sectionTitle('USAGE BY ORIGIN', report.agentDir, ctx)]
|
|
100
|
+
if (report.aggregation.byOrigin.length === 0) {
|
|
101
|
+
sections.push(dim('No assistant turns recorded yet.', ctx))
|
|
102
|
+
return sections.join('\n')
|
|
103
|
+
}
|
|
104
|
+
sections.push(renderOriginTable(report.aggregation.byOrigin, ctx))
|
|
105
|
+
return sections.join('\n')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Single source of truth for the origin breakdown table used by both the
|
|
109
|
+
// summary view and the dedicated `usage origin` subcommand. Matches the
|
|
110
|
+
// column shape of renderTotalsTable's other callers (byDay, byModel) plus
|
|
111
|
+
// one extra column for session count — deliberately plain numbers rather
|
|
112
|
+
// than a proportional bar, since the Cost column already lets the eye sort
|
|
113
|
+
// rows by spend and an extra "share of max" bar adds no information that
|
|
114
|
+
// isn't already in the report.
|
|
115
|
+
function renderOriginTable(byOrigin: readonly OriginUsage[], ctx: RenderCtx): string {
|
|
116
|
+
const rows = byOrigin.map((o) => ({
|
|
117
|
+
label: renderOriginLabel(o.originKind, ctx),
|
|
118
|
+
totals: o as UsageTotals,
|
|
119
|
+
extra: String(o.sessionCount),
|
|
120
|
+
extraTruncatable: false,
|
|
121
|
+
}))
|
|
122
|
+
return renderTotalsTable(rows, ctx, {
|
|
123
|
+
extraHeader: 'Sessions',
|
|
124
|
+
total: totalOfRows(rows),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Glyph + colored label for each origin kind. The glyphs are chosen to be
|
|
129
|
+
// instantly readable in a dense table: ▶ for TUI (interactive), ⏱ for cron
|
|
130
|
+
// (time-triggered), # for channel (chat-room sigil), ↳ for subagent (child),
|
|
131
|
+
// ? for unknown. ASCII fallbacks are not provided — the codebase already
|
|
132
|
+
// requires unicode for `…` `●` `✓` etc. on other commands.
|
|
133
|
+
function renderOriginLabel(kind: OriginKind, ctx: RenderCtx): string {
|
|
134
|
+
switch (kind) {
|
|
135
|
+
case 'tui':
|
|
136
|
+
return `${color('cyan', '▶', ctx)} ${'tui'}`
|
|
137
|
+
case 'cron':
|
|
138
|
+
return `${color('magenta', '⏱', ctx)} ${'cron'}`
|
|
139
|
+
case 'channel':
|
|
140
|
+
return `${color('green', '#', ctx)} ${'channel'}`
|
|
141
|
+
case 'subagent':
|
|
142
|
+
return `${color('yellow', '↳', ctx)} ${'subagent'}`
|
|
143
|
+
case 'unknown':
|
|
144
|
+
return `${dim('?', ctx)} ${dim('unknown', ctx)}`
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Sparkline trend across the full byDay range, scaled to the row's max cost.
|
|
149
|
+
// Returns null when there are fewer than 2 days (a single point conveys no
|
|
150
|
+
// trend information). The 8-level Unicode block scale `▁▂▃▄▅▆▇█` lets us
|
|
151
|
+
// pack ~80 days into a one-line glance — wider than any table-based view
|
|
152
|
+
// could fit at terminal widths under ~160 columns.
|
|
153
|
+
const SPARK_GLYPHS = '▁▂▃▄▅▆▇█'
|
|
154
|
+
|
|
155
|
+
function renderDailyTrend(byDay: readonly { date: string; cost: number }[], ctx: RenderCtx): string | null {
|
|
156
|
+
if (byDay.length < 2) return null
|
|
157
|
+
const costs = byDay.map((d) => d.cost)
|
|
158
|
+
const max = costs.reduce((m, c) => Math.max(m, c), 0)
|
|
159
|
+
if (max <= 0) return null
|
|
160
|
+
const spark = costs
|
|
161
|
+
.map((c) => {
|
|
162
|
+
const idx = Math.min(SPARK_GLYPHS.length - 1, Math.max(0, Math.round((c / max) * (SPARK_GLYPHS.length - 1))))
|
|
163
|
+
return SPARK_GLYPHS[idx]!
|
|
164
|
+
})
|
|
165
|
+
.join('')
|
|
166
|
+
const first = byDay[0]!.date
|
|
167
|
+
const last = byDay[byDay.length - 1]!.date
|
|
168
|
+
return `${dim('Trend (cost):', ctx)} ${color('cyan', spark, ctx)} ${dim(`${first} → ${last}`, ctx)}`
|
|
169
|
+
}
|
|
170
|
+
|
|
87
171
|
function renderDaily(report: UsageReport, ctx: RenderCtx, limit: number | undefined): string {
|
|
88
172
|
const days = limit !== undefined ? report.aggregation.byDay.slice(-limit) : report.aggregation.byDay
|
|
89
173
|
if (days.length === 0) return dim('No usage in range.', ctx)
|
|
@@ -100,8 +184,9 @@ function renderSessions(report: UsageReport, ctx: RenderCtx, limit: number): str
|
|
|
100
184
|
const rows = sessions.map((s) => {
|
|
101
185
|
const firstModel = s.models[0]
|
|
102
186
|
const extra = s.models.length > 1 ? `${s.models.length} models` : modelIdFromKey(firstModel)
|
|
187
|
+
const originGlyph = originGlyphOnly(s.originKind, ctx)
|
|
103
188
|
return {
|
|
104
|
-
label: `${color('magenta', s.sessionId.slice(0, 12), ctx)} ${dim(isoDay(s.firstAt), ctx)}`,
|
|
189
|
+
label: `${originGlyph} ${color('magenta', s.sessionId.slice(0, 12), ctx)} ${dim(isoDay(s.firstAt), ctx)}`,
|
|
105
190
|
totals: s as UsageTotals,
|
|
106
191
|
extra,
|
|
107
192
|
extraTruncatable: s.models.length === 1,
|
|
@@ -113,6 +198,21 @@ function renderSessions(report: UsageReport, ctx: RenderCtx, limit: number): str
|
|
|
113
198
|
].join('\n')
|
|
114
199
|
}
|
|
115
200
|
|
|
201
|
+
function originGlyphOnly(kind: OriginKind, ctx: RenderCtx): string {
|
|
202
|
+
switch (kind) {
|
|
203
|
+
case 'tui':
|
|
204
|
+
return color('cyan', '▶', ctx)
|
|
205
|
+
case 'cron':
|
|
206
|
+
return color('magenta', '⏱', ctx)
|
|
207
|
+
case 'channel':
|
|
208
|
+
return color('green', '#', ctx)
|
|
209
|
+
case 'subagent':
|
|
210
|
+
return color('yellow', '↳', ctx)
|
|
211
|
+
case 'unknown':
|
|
212
|
+
return dim('?', ctx)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
116
216
|
function renderModels(report: UsageReport, ctx: RenderCtx, limit: number | undefined): string {
|
|
117
217
|
const models = limit !== undefined ? report.aggregation.byModel.slice(0, limit) : report.aggregation.byModel
|
|
118
218
|
if (models.length === 0) return dim('No models in range.', ctx)
|
package/src/usage/scan.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
|
+
// Recognised origin kinds. Keep aligned with SessionOrigin's discriminator in
|
|
5
|
+
// src/agent/session-origin.ts. The 'unknown' bucket catches sessions written
|
|
6
|
+
// before origin stamping landed AND sessions whose session-meta line is
|
|
7
|
+
// malformed or missing — surfacing them under one explicit label is more
|
|
8
|
+
// honest than silently dropping them.
|
|
9
|
+
export const ORIGIN_KINDS = ['tui', 'cron', 'channel', 'subagent', 'unknown'] as const
|
|
10
|
+
export type OriginKind = (typeof ORIGIN_KINDS)[number]
|
|
11
|
+
|
|
4
12
|
// Narrow projection: session files can grow into tens of MB on long-lived
|
|
5
13
|
// agents, so we deliberately drop content/tool blocks before aggregation.
|
|
6
14
|
export type AssistantRow = {
|
|
@@ -15,6 +23,7 @@ export type AssistantRow = {
|
|
|
15
23
|
cacheWrite: number
|
|
16
24
|
totalTokens: number
|
|
17
25
|
cost: number
|
|
26
|
+
originKind: OriginKind
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export type ScanOptions = {
|
|
@@ -66,6 +75,13 @@ async function* readSessionFile(file: string, opts: ScanOptions): AsyncGenerator
|
|
|
66
75
|
}
|
|
67
76
|
const decoder = new TextDecoder()
|
|
68
77
|
let buf = ''
|
|
78
|
+
// First-stamp-wins per file. Once a `typeclaw.session-meta` custom entry
|
|
79
|
+
// pins the origin, later entries with the same customType are ignored —
|
|
80
|
+
// session-resume code paths may legitimately re-stamp on reopen, and the
|
|
81
|
+
// earliest one is the authoritative one for the session's first turn.
|
|
82
|
+
// Stays 'unknown' for legacy files (no stamp at all) so usage attribution
|
|
83
|
+
// surfaces them as a distinct bucket rather than dropping the rows.
|
|
84
|
+
const ctx: ParseCtx = { originKind: 'unknown', originPinned: false }
|
|
69
85
|
try {
|
|
70
86
|
for await (const chunk of stream) {
|
|
71
87
|
buf += decoder.decode(chunk, { stream: true })
|
|
@@ -73,7 +89,7 @@ async function* readSessionFile(file: string, opts: ScanOptions): AsyncGenerator
|
|
|
73
89
|
while (nl !== -1) {
|
|
74
90
|
const line = buf.slice(0, nl)
|
|
75
91
|
buf = buf.slice(nl + 1)
|
|
76
|
-
const row = parseLine(line, file, basename, opts)
|
|
92
|
+
const row = parseLine(line, file, basename, opts, ctx)
|
|
77
93
|
if (row !== null) yield row
|
|
78
94
|
nl = buf.indexOf('\n')
|
|
79
95
|
}
|
|
@@ -86,17 +102,20 @@ async function* readSessionFile(file: string, opts: ScanOptions): AsyncGenerator
|
|
|
86
102
|
// half-written record from a live writer is silently skipped (parseLine
|
|
87
103
|
// returns null and does NOT warn for the tail).
|
|
88
104
|
if (buf.length > 0) {
|
|
89
|
-
const row = parseLine(buf, file, basename, opts, { isTail: true })
|
|
105
|
+
const row = parseLine(buf, file, basename, opts, ctx, { isTail: true })
|
|
90
106
|
if (row !== null) yield row
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
109
|
|
|
110
|
+
type ParseCtx = { originKind: OriginKind; originPinned: boolean }
|
|
111
|
+
|
|
94
112
|
function parseLine(
|
|
95
113
|
line: string,
|
|
96
114
|
file: string,
|
|
97
115
|
basename: string,
|
|
98
116
|
opts: ScanOptions,
|
|
99
|
-
ctx:
|
|
117
|
+
ctx: ParseCtx,
|
|
118
|
+
flags: { isTail?: boolean } = {},
|
|
100
119
|
): AssistantRow | null {
|
|
101
120
|
const trimmed = line.trim()
|
|
102
121
|
if (trimmed === '') return null
|
|
@@ -106,7 +125,18 @@ function parseLine(
|
|
|
106
125
|
entry = JSON.parse(trimmed)
|
|
107
126
|
} catch {
|
|
108
127
|
// Silently skip the trailing tail: a live writer may be mid-append.
|
|
109
|
-
if (
|
|
128
|
+
if (flags.isTail !== true) opts.onWarn?.(`skipping malformed JSONL line in ${basename}`)
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isSessionMetaCustomEntry(entry)) {
|
|
133
|
+
if (!ctx.originPinned) {
|
|
134
|
+
const kind = entry.data.origin.kind
|
|
135
|
+
if ((ORIGIN_KINDS as readonly string[]).includes(kind)) {
|
|
136
|
+
ctx.originKind = kind as OriginKind
|
|
137
|
+
ctx.originPinned = true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
110
140
|
return null
|
|
111
141
|
}
|
|
112
142
|
|
|
@@ -134,9 +164,34 @@ function parseLine(
|
|
|
134
164
|
cacheWrite: numberOrZero(u.cacheWrite),
|
|
135
165
|
totalTokens: numberOrZero(u.totalTokens),
|
|
136
166
|
cost: numberOrZero(u.cost?.total),
|
|
167
|
+
originKind: ctx.originKind,
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
170
|
|
|
171
|
+
// Pi-coding-agent persists `appendCustomEntry(customType, data)` calls as
|
|
172
|
+
// `{type:"custom", customType, data, id, parentId, timestamp}` lines. We
|
|
173
|
+
// stamp our origin block with customType `typeclaw.session-meta` (constant
|
|
174
|
+
// kept in src/agent/session-meta.ts; duplicated as a literal here to keep
|
|
175
|
+
// the usage subsystem free of agent-stack imports — a Grep across the repo
|
|
176
|
+
// is the chosen drift guard).
|
|
177
|
+
type SessionMetaCustomEntry = {
|
|
178
|
+
type: 'custom'
|
|
179
|
+
customType: 'typeclaw.session-meta'
|
|
180
|
+
data: { origin: { kind: string } }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isSessionMetaCustomEntry(value: unknown): value is SessionMetaCustomEntry {
|
|
184
|
+
if (typeof value !== 'object' || value === null) return false
|
|
185
|
+
const v = value as Record<string, unknown>
|
|
186
|
+
if (v.type !== 'custom') return false
|
|
187
|
+
if (v.customType !== 'typeclaw.session-meta') return false
|
|
188
|
+
if (typeof v.data !== 'object' || v.data === null) return false
|
|
189
|
+
const d = v.data as Record<string, unknown>
|
|
190
|
+
if (typeof d.origin !== 'object' || d.origin === null) return false
|
|
191
|
+
const o = d.origin as Record<string, unknown>
|
|
192
|
+
return typeof o.kind === 'string'
|
|
193
|
+
}
|
|
194
|
+
|
|
140
195
|
type MessageEntry = { type: 'message'; message: { role: string; [k: string]: unknown } }
|
|
141
196
|
type AssistantMessageShape = {
|
|
142
197
|
role: 'assistant'
|