@tangle-network/harness-sessions 0.1.6 → 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/dist/builder.d.ts CHANGED
@@ -3,7 +3,6 @@ import type { SourceStats } from './source.js';
3
3
  export type BuildMode = 'full' | 'summary';
4
4
  /** An ISO-8601 instant from an ISO string, epoch milliseconds or epoch seconds; null otherwise. */
5
5
  export declare function isoTime(value: unknown): string | null;
6
- export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
7
6
  export declare const str: (value: unknown) => string | null;
8
7
  export declare function count(value: unknown): number | null;
9
8
  /** Parse tool arguments recorded as text; keep the text when it is not JSON. */
package/dist/builder.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  import { HARNESS_SESSION_SCHEMA } from './schema.js';
2
3
  /** An ISO-8601 instant from an ISO string, epoch milliseconds or epoch seconds; null otherwise. */
3
4
  export function isoTime(value) {
@@ -11,7 +12,6 @@ export function isoTime(value) {
11
12
  }
12
13
  return null;
13
14
  }
14
- export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
15
15
  export const str = (value) => (typeof value === 'string' ? value : null);
16
16
  export function count(value) {
17
17
  return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
package/dist/capture.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  /**
2
3
  * Reading the native sessions in a capture.
3
4
  *
@@ -11,7 +12,7 @@
11
12
  */
12
13
  import { readdir, readFile } from 'node:fs/promises';
13
14
  import { join } from 'node:path';
14
- import { isRecord, str } from './builder.js';
15
+ import { str } from './builder.js';
15
16
  import { opencodeSessionsInStore } from './formats/opencode.js';
16
17
  import { maybeReaderFor, readerFor, readerForFormat } from './registry.js';
17
18
  import { isMissing } from './source.js';
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  /**
2
3
  * Claude Code: `~/.claude/projects/<cwd slug>/<session id>.jsonl`, one JSON record per line, and
3
4
  * each subagent's own transcript at `<session id>/subagents/agent-<agent id>.jsonl`.
@@ -9,7 +10,7 @@
9
10
  * the API answered with.
10
11
  */
11
12
  import { basename, join } from 'node:path';
12
- import { SessionBuilder, count, isRecord, sessionError, str, textOf, usageOrNull } from '../builder.js';
13
+ import { SessionBuilder, count, sessionError, str, textOf, usageOrNull } from '../builder.js';
13
14
  import { fileRecords } from '../source.js';
14
15
  import { runFold } from './fold.js';
15
16
  import { EVIDENCE, LOCATE_READS, STORES, mapLimit, mtimeMs, storeGlob, walkFiles, withinPath } from '../stores.js';
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  /**
2
3
  * Codex: `~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<thread id>.jsonl`, one
3
4
  * `{timestamp, type, payload}` record per line.
@@ -10,7 +11,7 @@
10
11
  * (`total_token_usage`); the cumulative counter is the session total.
11
12
  */
12
13
  import { basename } from 'node:path';
13
- import { SessionBuilder, argumentsOf, count, isRecord, sessionError, str, textOf, usageOrNull, } from '../builder.js';
14
+ import { SessionBuilder, argumentsOf, count, sessionError, str, textOf, usageOrNull, } from '../builder.js';
14
15
  import { fileRecords, isMissing } from '../source.js';
15
16
  import { runFold } from './fold.js';
16
17
  import { CODEX_HOME_RULE, EVIDENCE, LOCATE_READS, STORES, codexHomes, mapLimit, storeGlob, walkFiles, withinPath } from '../stores.js';
@@ -66,14 +67,59 @@ function contentParts(content) {
66
67
  }
67
68
  return parts;
68
69
  }
69
- function outputOf(output) {
70
+ // `exec_command` and `write_stdin` answer with a text header (`Chunk ID`, `Wall time`, then
71
+ // `Process exited with code N` once the process is done) before `Output:`; a still-running process
72
+ // says `Process running with session ID` instead and has no status yet.
73
+ const PROCESS_EXIT = /^Process exited with code (-?\d+)\s*$/mu;
74
+ function processFailed(text) {
75
+ const header = text.split(/^Output:/mu, 1)[0] ?? '';
76
+ const match = PROCESS_EXIT.exec(header);
77
+ return match !== null && Number(match[1]) !== 0;
78
+ }
79
+ /** A nonzero numeric `exit_code` anywhere in a parsed JSON value (code-mode tool results nest it). */
80
+ function jsonExitFailed(value, depth = 0) {
81
+ if (depth > 4)
82
+ return false;
83
+ if (Array.isArray(value))
84
+ return value.some((v) => jsonExitFailed(v, depth + 1));
85
+ if (!isRecord(value))
86
+ return false;
87
+ if (typeof value.exit_code === 'number' && value.exit_code !== 0)
88
+ return true;
89
+ return Object.values(value).some((v) => jsonExitFailed(v, depth + 1));
90
+ }
91
+ /**
92
+ * Code mode runs JavaScript in an `exec` call; its `tools.exec_command(...)` results are printed
93
+ * as JSON items carrying `exit_code`. The first item is `Script failed` when the script threw.
94
+ */
95
+ function codeModeFailed(output) {
96
+ if (!Array.isArray(output))
97
+ return false;
98
+ return output.some((item, i) => {
99
+ const text = isRecord(item) && typeof item.text === 'string' ? item.text : '';
100
+ if (i === 0 && /^Script failed\b/u.test(text))
101
+ return true;
102
+ if (!text.includes('exit_code'))
103
+ return false;
104
+ try {
105
+ return jsonExitFailed(JSON.parse(text));
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ });
111
+ }
112
+ function outputOf(output, tool) {
113
+ const execFailed = () => tool.name === 'exec_command' || tool.name === 'write_stdin'
114
+ ? typeof output === 'string' && processFailed(output)
115
+ : tool.name === 'exec' && tool.input.includes('exec_command') && codeModeFailed(output);
70
116
  if (typeof output === 'string')
71
- return { output, text: output, isError: false };
117
+ return { output, text: output, isError: execFailed() };
72
118
  if (isRecord(output)) {
73
119
  const content = output.content ?? output.output;
74
120
  return { output, text: textOf(content) ?? (typeof content === 'string' ? content : null), isError: output.success === false };
75
121
  }
76
- return { output: output ?? null, text: textOf(output), isError: false };
122
+ return { output: output ?? null, text: textOf(output), isError: execFailed() };
77
123
  }
78
124
  const MODEL_OUTPUT = new Set(['message', 'reasoning']);
79
125
  export function createFold(ref, mode, stats) {
@@ -97,6 +143,7 @@ export function createFold(ref, mode, stats) {
97
143
  let inherited = false;
98
144
  let inheritedRecords = 0;
99
145
  const foreignThreads = new Set();
146
+ const tools = new Map();
100
147
  const openCall = (at) => {
101
148
  if (open)
102
149
  return open;
@@ -196,6 +243,7 @@ export function createFold(ref, mode, stats) {
196
243
  const raw = kind === 'function_call' ? payload.arguments : kind === 'custom_tool_call' ? payload.input : (payload.action ?? payload.arguments ?? payload.input);
197
244
  const call = openCall(at);
198
245
  assistantMessage(at, call, [{ type: 'tool-call', toolCallId: callId }]);
246
+ tools.set(callId, { name, input: typeof raw === 'string' ? raw : '' });
199
247
  builder.toolCall({ id: callId, name, ...argumentsOf(raw), at, messageId: openMessage });
200
248
  // A web search is answered inside the response; there is no output item.
201
249
  if (kind === 'web_search_call')
@@ -203,7 +251,7 @@ export function createFold(ref, mode, stats) {
203
251
  }
204
252
  else if (kind.endsWith('_call_output')) {
205
253
  const callId = str(payload.call_id) ?? '';
206
- const result = outputOf(payload.output);
254
+ const result = outputOf(payload.output, tools.get(callId) ?? { name: '', input: '' });
207
255
  const id = `${callId}:output`;
208
256
  builder.message({ id, role: 'tool', actor: 'tool-result', at, modelCallId: null, parts: [{ type: 'tool-result', toolCallId: callId }] });
209
257
  builder.toolResult(callId, { ...result, at, messageId: id });
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  /**
2
3
  * Factory Droid: `~/.factory/sessions/<encoded cwd>/<session id>.jsonl` plus a
3
4
  * `<session id>.settings.json` beside it.
@@ -10,7 +11,7 @@
10
11
  */
11
12
  import { readFile } from 'node:fs/promises';
12
13
  import { basename, join } from 'node:path';
13
- import { SessionBuilder, count, isRecord, str, textOf, usageOrNull } from '../builder.js';
14
+ import { SessionBuilder, count, str, textOf, usageOrNull } from '../builder.js';
14
15
  import { fileRecords, isMissing } from '../source.js';
15
16
  import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles, withinPath } from '../stores.js';
16
17
  import { runFold } from './fold.js';
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  /**
2
3
  * Kimi Code: one directory per session, `~/.kimi/sessions/<workdir hash>/<session id>/`, holding
3
4
  * `wire.jsonl` (the event stream this reader folds) and `context.jsonl` (the model context).
@@ -11,7 +12,7 @@
11
12
  * subagent's events arrive wrapped in `SubagentEvent`. The wire records no model.
12
13
  */
13
14
  import { basename, dirname, join } from 'node:path';
14
- import { SessionBuilder, count, isRecord, sessionError, str, textOf, usageOrNull } from '../builder.js';
15
+ import { SessionBuilder, count, sessionError, str, textOf, usageOrNull } from '../builder.js';
15
16
  import { fileRecords } from '../source.js';
16
17
  import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles } from '../stores.js';
17
18
  import { runFold } from './fold.js';
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  /**
2
3
  * OpenCode 1.x: one SQLite store for every session in the HOME,
3
4
  * `~/.local/share/opencode/opencode.db` (with `-wal` and `-shm` siblings while it is open).
@@ -12,7 +13,7 @@
12
13
  */
13
14
  import { createHash } from 'node:crypto';
14
15
  import { join } from 'node:path';
15
- import { SessionBuilder, count, isRecord, isoTime, sessionError, str, usageOrNull } from '../builder.js';
16
+ import { SessionBuilder, count, isoTime, sessionError, str, usageOrNull } from '../builder.js';
16
17
  import { isMissing, withSqlite } from '../source.js';
17
18
  import { EVIDENCE, STORES, mtimeMs, withinPath } from '../stores.js';
18
19
  const FORMAT = 'opencode.sqlite';
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@tangle-network/agent-core';
1
2
  /**
2
3
  * Pi: `~/.pi/agent/sessions/<encoded cwd>/<timestamp>_<session id>.jsonl`, one entry per line.
3
4
  *
@@ -9,7 +10,7 @@
9
10
  * `toolCallId`. Entries form a tree through `parentId`; branches share one file.
10
11
  */
11
12
  import { basename, join } from 'node:path';
12
- import { SessionBuilder, count, isRecord, sessionError, str, textOf, usageOrNull } from '../builder.js';
13
+ import { SessionBuilder, count, sessionError, str, textOf, usageOrNull } from '../builder.js';
13
14
  import { fileRecords } from '../source.js';
14
15
  import { runFold } from './fold.js';
15
16
  import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles, withinPath } from '../stores.js';
@@ -1,4 +1,4 @@
1
- import { isRecord } from '../builder.js';
1
+ import { isRecord } from '@tangle-network/agent-core';
2
2
  export function summarizeText(text, n) {
3
3
  const t = text.replace(/\s+/gu, ' ').trim();
4
4
  return t.length <= n ? t : `${t.slice(0, n - 1)}…`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/harness-sessions",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "One reader for every coding-agent harness's native session (Claude Code, Codex, OpenCode, Pi): store layouts, a normalized session with messages, tool calls, served models, usage and ending errors, and projections to chat messages, OTLP spans and turns",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -40,6 +40,7 @@
40
40
  "LICENSE"
41
41
  ],
42
42
  "dependencies": {
43
+ "@tangle-network/agent-core": "^0.13.0",
43
44
  "@tangle-network/agent-trace-contract": "^2.0.0"
44
45
  },
45
46
  "devDependencies": {