@agentic-kit/dsh 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.
- package/LICENSE +23 -0
- package/README.md +132 -0
- package/dsh-tool.d.ts +32 -0
- package/dsh-tool.js +87 -0
- package/dsh-types.d.ts +119 -0
- package/dsh-types.js +19 -0
- package/esm/dsh-tool.d.ts +32 -0
- package/esm/dsh-tool.js +83 -0
- package/esm/dsh-types.d.ts +119 -0
- package/esm/dsh-types.js +18 -0
- package/esm/index.d.ts +27 -0
- package/esm/index.js +26 -0
- package/esm/plugin.d.ts +37 -0
- package/esm/plugin.js +72 -0
- package/esm/schema.d.ts +15 -0
- package/esm/schema.js +148 -0
- package/esm/transcript.d.ts +48 -0
- package/esm/transcript.js +285 -0
- package/index.d.ts +27 -0
- package/index.js +40 -0
- package/package.json +46 -0
- package/plugin.d.ts +37 -0
- package/plugin.js +76 -0
- package/schema.d.ts +15 -0
- package/schema.js +153 -0
- package/transcript.d.ts +48 -0
- package/transcript.js +290 -0
package/schema.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toDshParameters = void 0;
|
|
4
|
+
exports.convertDshParameters = convertDshParameters;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
/**
|
|
7
|
+
* A neutral tool's zod parameters, as dsh's JSON Schema subset.
|
|
8
|
+
*
|
|
9
|
+
* dsh enforces a deliberately small schema vocabulary on every registered tool:
|
|
10
|
+
* `type`, `properties`, `required`, `items`, `oneOf`, `enum`, `const`, a boolean
|
|
11
|
+
* `additionalProperties`, and the annotations. A zod schema routinely produces
|
|
12
|
+
* more than that — `format` from `.uuid()`, `minimum`, `minItems`, `anyOf` from
|
|
13
|
+
* a union, an object-valued `additionalProperties` from a record, `$defs`/`$ref`
|
|
14
|
+
* from a reused sub-schema — and dsh rejects a tool carrying any of them.
|
|
15
|
+
*
|
|
16
|
+
* So this narrows: unsupported *constraints* are dropped, and unsupported
|
|
17
|
+
* *structure* throws. Dropping a constraint is safe here and only here, because
|
|
18
|
+
* the schema dsh receives is a hint to the model, not the enforcement:
|
|
19
|
+
* `toDshTool` parses the model's arguments with the tool's own zod schema
|
|
20
|
+
* before the body runs, so every constraint this drops is still applied — by
|
|
21
|
+
* the party that owns it. What cannot degrade is the shape a caller has to
|
|
22
|
+
* satisfy, which is why a non-object root or an unresolvable `$ref` is an error
|
|
23
|
+
* rather than an open schema.
|
|
24
|
+
*/
|
|
25
|
+
const CONSTRAINTS = new Set([
|
|
26
|
+
'type',
|
|
27
|
+
'properties',
|
|
28
|
+
'required',
|
|
29
|
+
'items',
|
|
30
|
+
'oneOf',
|
|
31
|
+
'enum',
|
|
32
|
+
'const',
|
|
33
|
+
'additionalProperties'
|
|
34
|
+
]);
|
|
35
|
+
const ANNOTATIONS = new Set(['description', 'title', 'default', 'examples']);
|
|
36
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
+
function narrow(node, path, dropped) {
|
|
38
|
+
if (!isRecord(node))
|
|
39
|
+
return {};
|
|
40
|
+
if (typeof node.$ref === 'string') {
|
|
41
|
+
throw new Error(`tool parameters use a JSON Schema $ref ("${node.$ref}") at ${path || 'the root'}; ` +
|
|
42
|
+
'dsh reads no references, so the schema must be inlined');
|
|
43
|
+
}
|
|
44
|
+
const out = {};
|
|
45
|
+
const source = widenUnions(node, path, dropped);
|
|
46
|
+
for (const [key, value] of Object.entries(source)) {
|
|
47
|
+
const at = path === '' ? key : `${path}.${key}`;
|
|
48
|
+
if (key === '$schema' || key === '$defs' || key === 'definitions')
|
|
49
|
+
continue;
|
|
50
|
+
if (ANNOTATIONS.has(key)) {
|
|
51
|
+
out[key] = value;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!CONSTRAINTS.has(key)) {
|
|
55
|
+
dropped.push(at);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
switch (key) {
|
|
59
|
+
case 'properties': {
|
|
60
|
+
const properties = {};
|
|
61
|
+
for (const [name, sub] of Object.entries(isRecord(value) ? value : {})) {
|
|
62
|
+
properties[name] = narrow(sub, `${at}.${name}`, dropped);
|
|
63
|
+
}
|
|
64
|
+
out.properties = properties;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case 'items':
|
|
68
|
+
out.items = narrow(value, at, dropped);
|
|
69
|
+
break;
|
|
70
|
+
case 'oneOf':
|
|
71
|
+
out.oneOf = (Array.isArray(value) ? value : []).map((sub, index) => narrow(sub, `${at}[${index}]`, dropped));
|
|
72
|
+
break;
|
|
73
|
+
case 'additionalProperties':
|
|
74
|
+
// Only the boolean form exists in dsh's subset; a zod record's
|
|
75
|
+
// object-valued form degrades to the open default.
|
|
76
|
+
if (typeof value === 'boolean')
|
|
77
|
+
out.additionalProperties = value;
|
|
78
|
+
else
|
|
79
|
+
dropped.push(at);
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
out[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// `required` may not name a property the narrowed schema no longer declares.
|
|
86
|
+
if (Array.isArray(out.required)) {
|
|
87
|
+
const declared = new Set(Object.keys(out.properties ?? {}));
|
|
88
|
+
const kept = out.required.filter((name) => typeof name === 'string' && declared.has(name));
|
|
89
|
+
if (kept.length === 0)
|
|
90
|
+
delete out.required;
|
|
91
|
+
else
|
|
92
|
+
out.required = kept;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `anyOf` and a type array as dsh's `oneOf`, where that is sound.
|
|
98
|
+
*
|
|
99
|
+
* zod emits `anyOf` for a union and `type: ['string', 'null']` for a nullable;
|
|
100
|
+
* dsh has neither, only `oneOf` — which validates *exactly one* branch. That is
|
|
101
|
+
* the same thing as `anyOf` only when the branches are disjoint, which is true
|
|
102
|
+
* of the shapes zod actually produces here (a nullable, a union of distinct
|
|
103
|
+
* scalar types) and not true in general. So a disjoint union converts, and an
|
|
104
|
+
* overlapping one degrades to unconstrained rather than becoming a schema that
|
|
105
|
+
* rejects a legitimate argument.
|
|
106
|
+
*/
|
|
107
|
+
function widenUnions(node, path, dropped) {
|
|
108
|
+
const rest = { ...node };
|
|
109
|
+
let branches;
|
|
110
|
+
if (Array.isArray(rest.anyOf)) {
|
|
111
|
+
branches = rest.anyOf;
|
|
112
|
+
delete rest.anyOf;
|
|
113
|
+
}
|
|
114
|
+
else if (Array.isArray(rest.type)) {
|
|
115
|
+
branches = rest.type.map((type) => ({ type }));
|
|
116
|
+
delete rest.type;
|
|
117
|
+
}
|
|
118
|
+
if (!branches || rest.oneOf !== undefined)
|
|
119
|
+
return node;
|
|
120
|
+
const types = branches.map((branch) => isRecord(branch) && typeof branch.type === 'string' ? branch.type : undefined);
|
|
121
|
+
const disjoint = branches.length >= 2 &&
|
|
122
|
+
types.every((type) => type !== undefined) &&
|
|
123
|
+
new Set(types).size === types.length;
|
|
124
|
+
if (!disjoint) {
|
|
125
|
+
dropped.push(path === '' ? 'anyOf' : `${path}.anyOf`);
|
|
126
|
+
return rest;
|
|
127
|
+
}
|
|
128
|
+
return { ...rest, oneOf: branches };
|
|
129
|
+
}
|
|
130
|
+
/** Convert and report, for a host that wants to see what degraded. */
|
|
131
|
+
function convertDshParameters(schema) {
|
|
132
|
+
const dropped = [];
|
|
133
|
+
// zod's own emitter, in input mode (what a *caller* must send) against
|
|
134
|
+
// draft-7 — the dialect dsh's subset is carved out of.
|
|
135
|
+
const jsonSchema = zod_1.z.toJSONSchema(schema, { target: 'draft-7', io: 'input' });
|
|
136
|
+
const narrowed = narrow(jsonSchema, '', dropped);
|
|
137
|
+
if (narrowed.type !== 'object') {
|
|
138
|
+
throw new Error(`tool parameters must be an object schema; received ${String(narrowed.type ?? 'no type')}. ` +
|
|
139
|
+
'dsh names every argument, so a tool cannot take a bare value or a top-level union');
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
parameters: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: narrowed.properties ?? {},
|
|
145
|
+
...(narrowed.required ? { required: narrowed.required } : {}),
|
|
146
|
+
...(narrowed.description ? { description: narrowed.description } : {})
|
|
147
|
+
},
|
|
148
|
+
dropped
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** The dsh-subset JSON Schema for a tool's parameters. */
|
|
152
|
+
const toDshParameters = (schema) => convertDshParameters(schema).parameters;
|
|
153
|
+
exports.toDshParameters = toDshParameters;
|
package/transcript.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness's transcript reader: dsh session events → neutral events.
|
|
3
|
+
*
|
|
4
|
+
* The read half of the adapter, and deliberately the only file in this package
|
|
5
|
+
* a renderer imports (`@agentic-kit/dsh/transcript`): it is browser-safe, has
|
|
6
|
+
* no dsh dependency and no db-tools dependency, so a dashboard can project a
|
|
7
|
+
* dsh run without pulling a node graph. `@agentic-kit/run-log` owns the neutral
|
|
8
|
+
* vocabulary and the registry; the format's meaning lives here, beside the
|
|
9
|
+
* adapter that produces it.
|
|
10
|
+
*
|
|
11
|
+
* dsh's log differs from pi's in three ways that matter:
|
|
12
|
+
* - it is an *event* log, not a message log: a tool call and its result are
|
|
13
|
+
* separate events with their own sequence numbers, and a step boundary is an
|
|
14
|
+
* event of its own;
|
|
15
|
+
* - `time` is epoch milliseconds, not an ISO string;
|
|
16
|
+
* - assistant reasoning is a `reasoning` content block, and a tool call's
|
|
17
|
+
* arguments arrive as the raw JSON string the model produced.
|
|
18
|
+
*
|
|
19
|
+
* Register the reader once at host startup, e.g.
|
|
20
|
+
* `transcriptReaders.register(dshTranscriptReader)`.
|
|
21
|
+
*/
|
|
22
|
+
import { type TranscriptEntry, type TranscriptEvent, type TranscriptReader } from '@agentic-kit/run-log';
|
|
23
|
+
/** dsh's session-event log (`@deepseek-ai/dsh-session`). */
|
|
24
|
+
export declare const DSH_TRANSCRIPT_FORMAT = "dsh";
|
|
25
|
+
/**
|
|
26
|
+
* dsh's `SESSION_FORMAT_VERSION` as of `0.1.0-rc.7`. It bumps only when the
|
|
27
|
+
* event envelope or the surface mechanism changes — a new event *type* does
|
|
28
|
+
* not bump it, which is why an unrecognized type here becomes an `unknown`
|
|
29
|
+
* event rather than a refusal.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SUPPORTED_DSH_TRANSCRIPT_VERSION = 0;
|
|
32
|
+
/** One entry of a dsh session log, structurally. */
|
|
33
|
+
export interface DshSessionEvent extends TranscriptEntry {
|
|
34
|
+
type: string;
|
|
35
|
+
seq?: number;
|
|
36
|
+
time?: number;
|
|
37
|
+
data?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Narrow an untrusted dsh event. `seq` and `time` are part of dsh's envelope
|
|
41
|
+
* rather than optional decoration, so an entry missing them is not a dsh event
|
|
42
|
+
* and must not be stored as one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertDshSessionEvent(value: unknown): DshSessionEvent;
|
|
45
|
+
/** What a single dsh event means, in order. */
|
|
46
|
+
export declare function dshEventToEvents(entry: TranscriptEntry): TranscriptEvent[];
|
|
47
|
+
/** dsh's session-event log, as a registrable reader. */
|
|
48
|
+
export declare const dshTranscriptReader: TranscriptReader;
|
package/transcript.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DeepSeek Harness's transcript reader: dsh session events → neutral events.
|
|
4
|
+
*
|
|
5
|
+
* The read half of the adapter, and deliberately the only file in this package
|
|
6
|
+
* a renderer imports (`@agentic-kit/dsh/transcript`): it is browser-safe, has
|
|
7
|
+
* no dsh dependency and no db-tools dependency, so a dashboard can project a
|
|
8
|
+
* dsh run without pulling a node graph. `@agentic-kit/run-log` owns the neutral
|
|
9
|
+
* vocabulary and the registry; the format's meaning lives here, beside the
|
|
10
|
+
* adapter that produces it.
|
|
11
|
+
*
|
|
12
|
+
* dsh's log differs from pi's in three ways that matter:
|
|
13
|
+
* - it is an *event* log, not a message log: a tool call and its result are
|
|
14
|
+
* separate events with their own sequence numbers, and a step boundary is an
|
|
15
|
+
* event of its own;
|
|
16
|
+
* - `time` is epoch milliseconds, not an ISO string;
|
|
17
|
+
* - assistant reasoning is a `reasoning` content block, and a tool call's
|
|
18
|
+
* arguments arrive as the raw JSON string the model produced.
|
|
19
|
+
*
|
|
20
|
+
* Register the reader once at host startup, e.g.
|
|
21
|
+
* `transcriptReaders.register(dshTranscriptReader)`.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.dshTranscriptReader = exports.SUPPORTED_DSH_TRANSCRIPT_VERSION = exports.DSH_TRANSCRIPT_FORMAT = void 0;
|
|
25
|
+
exports.assertDshSessionEvent = assertDshSessionEvent;
|
|
26
|
+
exports.dshEventToEvents = dshEventToEvents;
|
|
27
|
+
const run_log_1 = require("@agentic-kit/run-log");
|
|
28
|
+
/** dsh's session-event log (`@deepseek-ai/dsh-session`). */
|
|
29
|
+
exports.DSH_TRANSCRIPT_FORMAT = 'dsh';
|
|
30
|
+
/**
|
|
31
|
+
* dsh's `SESSION_FORMAT_VERSION` as of `0.1.0-rc.7`. It bumps only when the
|
|
32
|
+
* event envelope or the surface mechanism changes — a new event *type* does
|
|
33
|
+
* not bump it, which is why an unrecognized type here becomes an `unknown`
|
|
34
|
+
* event rather than a refusal.
|
|
35
|
+
*/
|
|
36
|
+
exports.SUPPORTED_DSH_TRANSCRIPT_VERSION = 0;
|
|
37
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
38
|
+
/**
|
|
39
|
+
* Narrow an untrusted dsh event. `seq` and `time` are part of dsh's envelope
|
|
40
|
+
* rather than optional decoration, so an entry missing them is not a dsh event
|
|
41
|
+
* and must not be stored as one.
|
|
42
|
+
*/
|
|
43
|
+
function assertDshSessionEvent(value) {
|
|
44
|
+
const entry = (0, run_log_1.assertTranscriptEntry)(value);
|
|
45
|
+
if (typeof entry.seq !== 'number' || !Number.isFinite(entry.seq)) {
|
|
46
|
+
throw new TypeError('dsh session event must carry a numeric `seq`');
|
|
47
|
+
}
|
|
48
|
+
if (typeof entry.time !== 'number' || !Number.isFinite(entry.time)) {
|
|
49
|
+
throw new TypeError('dsh session event must carry a numeric `time` (epoch ms)');
|
|
50
|
+
}
|
|
51
|
+
if (entry.data !== undefined && !isRecord(entry.data)) {
|
|
52
|
+
throw new TypeError('dsh session event `data` must be an object when present');
|
|
53
|
+
}
|
|
54
|
+
return entry;
|
|
55
|
+
}
|
|
56
|
+
/** What a single dsh event means, in order. */
|
|
57
|
+
function dshEventToEvents(entry) {
|
|
58
|
+
const event = entry;
|
|
59
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
60
|
+
const base = {
|
|
61
|
+
...(typeof event.seq === 'number' ? { entryId: String(event.seq) } : {}),
|
|
62
|
+
...(typeof event.time === 'number' ? { at: new Date(event.time).toISOString() } : {})
|
|
63
|
+
};
|
|
64
|
+
switch (event.type) {
|
|
65
|
+
case 'user/message': {
|
|
66
|
+
// A user-role event covers a human prompt and dsh's own injected context
|
|
67
|
+
// (file-change notices, skill content); `source.kind` tells them apart,
|
|
68
|
+
// and only a human one belongs in the conversation as a user turn.
|
|
69
|
+
const source = isRecord(data.source) ? data.source : {};
|
|
70
|
+
const text = blockText(data.content);
|
|
71
|
+
if (source.kind === 'user') {
|
|
72
|
+
return [{ kind: 'text', role: 'user', text, ...base }];
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
kind: 'custom',
|
|
77
|
+
customType: `dsh.context.${String(source.kind ?? 'unknown')}`,
|
|
78
|
+
text,
|
|
79
|
+
display: false,
|
|
80
|
+
...(source.plugin === undefined ? {} : { details: { plugin: source.plugin } }),
|
|
81
|
+
...base
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
case 'assistant/message': {
|
|
86
|
+
const message = isRecord(data.message) ? data.message : {};
|
|
87
|
+
const source = isRecord(message.source) ? message.source : {};
|
|
88
|
+
const model = typeof source.model === 'string' ? source.model : undefined;
|
|
89
|
+
const provider = typeof source.provider === 'string' ? source.provider : undefined;
|
|
90
|
+
const usage = tokenUsage(data.usage);
|
|
91
|
+
const events = [
|
|
92
|
+
{
|
|
93
|
+
kind: 'model-response',
|
|
94
|
+
...(model ? { model } : {}),
|
|
95
|
+
...(provider ? { provider } : {}),
|
|
96
|
+
...(usage ? { usage } : {}),
|
|
97
|
+
...base
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
101
|
+
if (!isRecord(block))
|
|
102
|
+
continue;
|
|
103
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text.length > 0) {
|
|
104
|
+
events.push({
|
|
105
|
+
kind: 'text',
|
|
106
|
+
role: 'assistant',
|
|
107
|
+
text: block.text,
|
|
108
|
+
...(model ? { model } : {}),
|
|
109
|
+
...(provider ? { provider } : {}),
|
|
110
|
+
...base
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else if (block.type === 'reasoning' && typeof block.text === 'string') {
|
|
114
|
+
events.push({ kind: 'thinking', text: block.text, ...base });
|
|
115
|
+
}
|
|
116
|
+
// A `tool-call` block is also logged as its own `tool/call` event, which
|
|
117
|
+
// is the one this reader projects — projecting both would double every
|
|
118
|
+
// call in a trace.
|
|
119
|
+
}
|
|
120
|
+
return events;
|
|
121
|
+
}
|
|
122
|
+
case 'tool/call':
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
kind: 'tool-call',
|
|
126
|
+
toolCallId: String(data.callId ?? ''),
|
|
127
|
+
name: String(data.name ?? ''),
|
|
128
|
+
arguments: parseArguments(data.arguments),
|
|
129
|
+
...base
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
case 'tool/result': {
|
|
133
|
+
const message = isRecord(data.message) ? data.message : {};
|
|
134
|
+
const block = (Array.isArray(message.content) ? message.content : []).find((candidate) => isRecord(candidate) && candidate.type === 'tool-result');
|
|
135
|
+
const source = isRecord(message.source) ? message.source : {};
|
|
136
|
+
const error = isRecord(data.error) ? data.error : undefined;
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
kind: 'tool-result',
|
|
140
|
+
toolCallId: String(block?.toolCallId ?? source.callId ?? ''),
|
|
141
|
+
// dsh's result carries the call id, not the tool name; a projector
|
|
142
|
+
// pairs it with the `tool/call` that named it.
|
|
143
|
+
name: '',
|
|
144
|
+
output: blockText(block?.content),
|
|
145
|
+
failed: block?.isError === true || error !== undefined,
|
|
146
|
+
...(data.meta === undefined ? {} : { details: data.meta }),
|
|
147
|
+
...base
|
|
148
|
+
}
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
case 'approval/asked': {
|
|
152
|
+
const callId = typeof data.callId === 'string' ? data.callId : undefined;
|
|
153
|
+
const reason = typeof data.reason === 'string' ? data.reason : '';
|
|
154
|
+
if (!callId)
|
|
155
|
+
break;
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
kind: 'custom',
|
|
159
|
+
customType: run_log_1.APPROVAL_REQUEST_TYPE,
|
|
160
|
+
text: reason || `Approve ${String(data.toolName ?? 'tool call')}?`,
|
|
161
|
+
display: true,
|
|
162
|
+
details: { toolCallId: callId },
|
|
163
|
+
...base
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
case 'approval/decided': {
|
|
168
|
+
const outcome = String(data.outcome ?? '');
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
kind: 'custom',
|
|
172
|
+
customType: run_log_1.APPROVAL_RESOLUTION_TYPE,
|
|
173
|
+
text: outcome,
|
|
174
|
+
display: true,
|
|
175
|
+
details: {
|
|
176
|
+
// dsh pairs a decision with its ask by approval id; the request
|
|
177
|
+
// carried the call id, so a projector joins through the ask.
|
|
178
|
+
approvalId: data.id,
|
|
179
|
+
decision: outcome === 'allowed-once' ? 'approved' : 'rejected',
|
|
180
|
+
reason: outcome
|
|
181
|
+
},
|
|
182
|
+
...base
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
case 'compaction/summary':
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
kind: 'summary',
|
|
190
|
+
reason: 'compaction',
|
|
191
|
+
summary: typeof data.summary === 'string' ? data.summary : blockText(data.content),
|
|
192
|
+
...base
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
case 'command/run':
|
|
196
|
+
return [
|
|
197
|
+
{
|
|
198
|
+
kind: 'bash',
|
|
199
|
+
command: String(data.command ?? ''),
|
|
200
|
+
output: '',
|
|
201
|
+
...base
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
case 'command/done':
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
kind: 'bash',
|
|
208
|
+
command: String(data.command ?? ''),
|
|
209
|
+
output: blockText(data.content) || String(data.output ?? ''),
|
|
210
|
+
...(typeof data.exitCode === 'number' ? { exitCode: data.exitCode } : {}),
|
|
211
|
+
...base
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
// Token-level replay of an `assistant/message` that is projected in full.
|
|
215
|
+
case 'assistant/chunk':
|
|
216
|
+
return [];
|
|
217
|
+
default:
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
return [{ kind: 'unknown', entryType: event.type, entry, ...base }];
|
|
221
|
+
}
|
|
222
|
+
/** dsh's session-event log, as a registrable reader. */
|
|
223
|
+
exports.dshTranscriptReader = {
|
|
224
|
+
format: exports.DSH_TRANSCRIPT_FORMAT,
|
|
225
|
+
version: exports.SUPPORTED_DSH_TRANSCRIPT_VERSION,
|
|
226
|
+
assertEntry: assertDshSessionEvent,
|
|
227
|
+
toEvents: dshEventToEvents
|
|
228
|
+
};
|
|
229
|
+
/** The text of a dsh content-block array. */
|
|
230
|
+
function blockText(content) {
|
|
231
|
+
if (typeof content === 'string')
|
|
232
|
+
return content;
|
|
233
|
+
if (!Array.isArray(content))
|
|
234
|
+
return '';
|
|
235
|
+
return content
|
|
236
|
+
.map((block) => {
|
|
237
|
+
if (!isRecord(block))
|
|
238
|
+
return '';
|
|
239
|
+
if (typeof block.text === 'string')
|
|
240
|
+
return block.text;
|
|
241
|
+
if (Array.isArray(block.content))
|
|
242
|
+
return blockText(block.content);
|
|
243
|
+
return '';
|
|
244
|
+
})
|
|
245
|
+
.filter((text) => text.length > 0)
|
|
246
|
+
.join('\n');
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A tool call's arguments. dsh logs the raw JSON string the model produced, so
|
|
250
|
+
* a malformed call is *in* the log — it becomes the string it was rather than
|
|
251
|
+
* failing the whole entry.
|
|
252
|
+
*/
|
|
253
|
+
function parseArguments(value) {
|
|
254
|
+
if (isRecord(value))
|
|
255
|
+
return value;
|
|
256
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
257
|
+
return {};
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(value);
|
|
260
|
+
return isRecord(parsed) ? parsed : { value: parsed };
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return { raw: value };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* dsh's `TokenUsage` in the neutral vocabulary. Its input counts are disjoint
|
|
268
|
+
* — cached input is reported apart from `inputTokens` — so a total is the sum
|
|
269
|
+
* rather than the input field.
|
|
270
|
+
*/
|
|
271
|
+
function tokenUsage(value) {
|
|
272
|
+
if (!isRecord(value))
|
|
273
|
+
return undefined;
|
|
274
|
+
const input = numeric(value.inputTokens);
|
|
275
|
+
const output = numeric(value.outputTokens);
|
|
276
|
+
const cacheRead = numeric(value.cacheReadTokens);
|
|
277
|
+
const cacheWrite = numeric(value.cacheWriteTokens);
|
|
278
|
+
const usage = {
|
|
279
|
+
...(input === undefined ? {} : { input }),
|
|
280
|
+
...(output === undefined ? {} : { output }),
|
|
281
|
+
...(cacheRead === undefined ? {} : { cacheRead }),
|
|
282
|
+
...(cacheWrite === undefined ? {} : { cacheWrite })
|
|
283
|
+
};
|
|
284
|
+
if (Object.keys(usage).length === 0)
|
|
285
|
+
return undefined;
|
|
286
|
+
usage.totalTokens =
|
|
287
|
+
(input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
|
|
288
|
+
return usage;
|
|
289
|
+
}
|
|
290
|
+
const numeric = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|