@ai-agent-forge/plugin-session-history 0.88.18 → 0.88.20
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/agent-forge.json +1 -1
- package/dist/history-tool.d.ts +2 -2
- package/dist/history-tool.d.ts.map +1 -1
- package/dist/history-tool.js +6 -2
- package/dist/history-tool.js.map +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
package/agent-forge.json
CHANGED
package/dist/history-tool.d.ts
CHANGED
|
@@ -73,8 +73,8 @@ export declare function historyToolDefinition(getSessionFile: () => string | und
|
|
|
73
73
|
scope: Type.TOptional<Type.TUnion<[Type.TLiteral<"active">, Type.TLiteral<"archive">]>>;
|
|
74
74
|
segmentId: Type.TOptional<Type.TString>;
|
|
75
75
|
query: Type.TOptional<Type.TString>;
|
|
76
|
-
offset: Type.TOptional<Type.
|
|
77
|
-
limit: Type.TOptional<Type.
|
|
76
|
+
offset: Type.TOptional<Type.TInteger>;
|
|
77
|
+
limit: Type.TOptional<Type.TInteger>;
|
|
78
78
|
}>;
|
|
79
79
|
readonlySafe: boolean;
|
|
80
80
|
execute: (first: unknown, second?: unknown) => Promise<HistoryResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history-tool.d.ts","sourceRoot":"","sources":["../src/history-tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG/B;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,sCAAsC;AACtC,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAErD,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAiD5D;AAED,oCAAoC;AACpC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GACtB;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC3B,GACD;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtG,CAAC;AAIL,wDAAwD;AACxD,wBAAsB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6CjG;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,MAAM,GAAG,SAAS;;;;;;;;;;;;;;EA+C7E","sourcesContent":["/**\n * `history` 工具:一个工具、三种 type(list / read / search)。\n *\n * 用户裁决 2026-10-20:检索类能力合并为单工具、用类型与参数区分,避免工具过细。\n * 参数组合 fail-closed 校验;每类给出单行用法,模型不必逐个探索。\n */\nimport { Type } from \"typebox\";\nimport { filterSegmentsByScope, readSegmentSlice, readSegmentsFromLog, searchSegment } from \"./history-store.ts\";\n\n/**\n * Number of most recent segments the active index holds. Mirrors the value the\n * compaction policy uses (plugins must not import across package roots, so the\n * constant is restated here); the split is by recency only, so a mismatch would\n * shift the active/archive boundary but never lose data.\n */\nexport const HISTORY_ACTIVE_SEGMENTS = 10;\n\n/** Tool name exposed to the model. */\nexport const HISTORY_TOOL_NAME = \"history\";\n\nexport type HistoryType = \"list\" | \"read\" | \"search\";\n\n/** Parsed tool input after validation. */\nexport interface HistoryInput {\n\treadonly type: HistoryType;\n\treadonly scope: \"active\" | \"archive\";\n\treadonly segmentId?: string;\n\treadonly query?: string;\n\treadonly offset?: number;\n\treadonly limit?: number;\n}\n\n/**\n * Validate one tool call. Throws on any combination the schema cannot rule out\n * alone — `read` without an id, `search` without a query, out-of-range paging,\n * or parameters that belong to a different type.\n */\nexport function parseHistoryInput(raw: unknown): HistoryInput {\n\tif (typeof raw !== \"object\" || raw === null) throw new Error(\"history requires an object input\");\n\tconst input = raw as Record<string, unknown>;\n\tconst type = input.type;\n\tif (type !== \"list\" && type !== \"read\" && type !== \"search\") {\n\t\tthrow new Error('history.type must be \"list\", \"read\" or \"search\"');\n\t}\n\t// Optional fields accept JSON null as omitted: real models serialize absent\n\t// optional parameters as null rather than dropping the key (observed in the\n\t// 2026-10-24 S4 long-run retrieval: two real calls were rejected until the\n\t// model gave up). Null carries no value, so it is absence, not a value; the\n\t// cross-type and range checks below still fail closed on real conflicts.\n\tconst scope = input.scope === undefined || input.scope === null ? \"active\" : input.scope;\n\tif (scope !== \"active\" && scope !== \"archive\") throw new Error('history.scope must be \"active\" or \"archive\"');\n\n\tconst segmentId =\n\t\ttypeof input.segmentId === \"string\" && input.segmentId.trim() !== \"\" ? input.segmentId.trim() : undefined;\n\tconst query = typeof input.query === \"string\" && input.query.trim() !== \"\" ? input.query.trim() : undefined;\n\tconst offset = input.offset === undefined || input.offset === null ? undefined : input.offset;\n\tconst limit = input.limit === undefined || input.limit === null ? undefined : input.limit;\n\tif (offset !== undefined && (typeof offset !== \"number\" || !Number.isInteger(offset) || offset < 0)) {\n\t\tthrow new Error(\"history.offset must be a non-negative integer\");\n\t}\n\tif (limit !== undefined && (typeof limit !== \"number\" || !Number.isInteger(limit) || limit < 1)) {\n\t\tthrow new Error(\"history.limit must be a positive integer\");\n\t}\n\n\tif (type === \"list\") {\n\t\tif (segmentId !== undefined || query !== undefined || offset !== undefined || limit !== undefined) {\n\t\t\tthrow new Error('history type \"list\" accepts only scope; segmentId/query/offset/limit belong to read/search');\n\t\t}\n\t\treturn { type, scope };\n\t}\n\tif (type === \"read\") {\n\t\tif (segmentId === undefined) throw new Error('history type \"read\" requires segmentId');\n\t\tif (query !== undefined) throw new Error('history type \"read\" does not accept query; use type \"search\"');\n\t\treturn {\n\t\t\ttype,\n\t\t\tscope,\n\t\t\tsegmentId,\n\t\t\t...(offset === undefined ? {} : { offset }),\n\t\t\t...(limit === undefined ? {} : { limit }),\n\t\t};\n\t}\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tif (segmentId !== undefined || offset !== undefined || limit !== undefined) {\n\t\tthrow new Error('history type \"search\" accepts query and scope only; read a hit with type \"read\"');\n\t}\n\treturn { type, scope, query };\n}\n\n/** Index row returned by `list`. */\nexport interface HistoryListRow {\n\treadonly id: string;\n\treadonly closedAt: number;\n\treadonly summary: string;\n\treadonly summaryTokens: number;\n}\n\n/** Result shapes, kept small and JSON-serializable for the tool boundary. */\nexport type HistoryResult =\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"list\";\n\t\t\treadonly scope: string;\n\t\t\treadonly rows: readonly HistoryListRow[];\n\t\t\treadonly note?: string;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"read\";\n\t\t\treadonly segmentId: string;\n\t\t\treadonly lines: readonly string[];\n\t\t\treadonly truncated: boolean;\n\t\t\treadonly totalLines: number;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"search\";\n\t\t\treadonly query: string;\n\t\t\treadonly hits: readonly { readonly segmentId: string; readonly line: number; readonly text: string }[];\n\t };\n\nconst CHARS_PER_TOKEN = 3.5;\n\n/** Execute a validated call against one session log. */\nexport async function runHistory(input: HistoryInput, sessionFile: string): Promise<HistoryResult> {\n\tconst segments = await readSegmentsFromLog(sessionFile);\n\tconst scoped = filterSegmentsByScope(segments, input.scope, HISTORY_ACTIVE_SEGMENTS);\n\n\tif (input.type === \"list\") {\n\t\tconst rows = scoped.map((segment) => ({\n\t\t\tid: segment.id,\n\t\t\tclosedAt: segment.closedAt,\n\t\t\tsummary: segment.summary,\n\t\t\tsummaryTokens: Math.ceil(segment.summary.length / CHARS_PER_TOKEN),\n\t\t}));\n\t\treturn {\n\t\t\tok: true,\n\t\t\ttype: \"list\",\n\t\t\tscope: input.scope,\n\t\t\trows,\n\t\t\t...(scoped.length === 0 ? { note: \"no closed segments yet; the live conversation is still in context\" } : {}),\n\t\t};\n\t}\n\n\tif (input.type === \"read\") {\n\t\tconst segment = scoped.find((item) => item.id === input.segmentId);\n\t\tif (segment === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`segment ${input.segmentId} not found in scope \"${input.scope}\"; call history type=\"list\" to see available ids`,\n\t\t\t);\n\t\t}\n\t\tconst result = await readSegmentSlice(segment, {\n\t\t\tcurrentSessionFile: sessionFile,\n\t\t\t...(input.offset === undefined ? {} : { offset: input.offset }),\n\t\t\t...(input.limit === undefined ? {} : { limit: input.limit }),\n\t\t});\n\t\treturn { ok: true, type: \"read\", segmentId: segment.id, ...result };\n\t}\n\n\t// `type === \"search\"` guarantees a query at runtime; the union type does not\n\t// carry that, so narrow once here instead of asserting at each use.\n\tconst query = input.query;\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tconst hits: { segmentId: string; line: number; text: string }[] = [];\n\tfor (const segment of scoped) {\n\t\tconst found = await searchSegment(segment, query, { currentSessionFile: sessionFile });\n\t\tfor (const hit of found) hits.push({ segmentId: segment.id, line: hit.line, text: hit.text });\n\t}\n\treturn { ok: true, type: \"search\", query, hits };\n}\n\n/** The tool definition registered with the capability API. */\nexport function historyToolDefinition(getSessionFile: () => string | undefined) {\n\treturn {\n\t\tname: HISTORY_TOOL_NAME,\n\t\tlabel: \"Session history\",\n\t\tdescription: [\n\t\t\t'Read earlier conversation segments that were cut out of context to bound its size. type=\"list\" shows the index (ids, time, summary);',\n\t\t\t'type=\"read\" returns a segment\\'s original entries (segmentId required; offset/limit page through it);',\n\t\t\t'type=\"search\" greps inside segments (query required). scope=\"archive\" reaches older segments that left the index.',\n\t\t].join(\" \"),\n\t\tparameters: Type.Object({\n\t\t\ttype: Type.Union([Type.Literal(\"list\"), Type.Literal(\"read\"), Type.Literal(\"search\")], {\n\t\t\t\tdescription:\n\t\t\t\t\t\"list = index of segments; read = original entries of one segment; search = grep inside segments\",\n\t\t\t}),\n\t\t\tscope: Type.Optional(\n\t\t\t\tType.Union([Type.Literal(\"active\"), Type.Literal(\"archive\")], {\n\t\t\t\t\tdescription: \"active (default) = segments still in the index; archive = older segments\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tsegmentId: Type.Optional(\n\t\t\t\tType.String({ description: 'Segment id from type=\"list\" (required for type=\"read\")' }),\n\t\t\t),\n\t\t\tquery: Type.Optional(Type.String({ description: 'Text to find (required for type=\"search\")' })),\n\t\t\toffset: Type.Optional(Type.Number({ description: 'type=\"read\": skip this many lines (default 0)' })),\n\t\t\tlimit: Type.Optional(\n\t\t\t\tType.Number({ description: 'type=\"read\": return at most this many lines (default 200)' }),\n\t\t\t),\n\t\t}),\n\t\treadonlySafe: true,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t// The capability API passes either (input), (input, context) or\n\t\t\t// (context, input) depending on the host shape (goal/todo-set use the\n\t\t\t// same objectInput helper). The 2026-10-24 S4 long-run hit the\n\t\t\t// (context, input) shape: every history call failed with \"requires an\n\t\t\t// object input\" until the model gave up. Pick the object argument and\n\t\t\t// unwrap the context envelope's `input` field when present.\n\t\t\tconst candidate = typeof first === \"object\" && first !== null && !Array.isArray(first) ? first : second;\n\t\t\tconst raw =\n\t\t\t\ttypeof candidate === \"object\" && candidate !== null && \"input\" in (candidate as object)\n\t\t\t\t\t? (candidate as { input: unknown }).input\n\t\t\t\t\t: candidate;\n\t\t\tconst sessionFile = getSessionFile();\n\t\t\tif (sessionFile === undefined) throw new Error(\"this session has no file on disk; history is unavailable\");\n\t\t\tconst input = parseHistoryInput(raw);\n\t\t\treturn await runHistory(input, sessionFile);\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"history-tool.d.ts","sourceRoot":"","sources":["../src/history-tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG/B;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,sCAAsC;AACtC,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAErD,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAiD5D;AAED,oCAAoC;AACpC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GACtB;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC3B,GACD;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtG,CAAC;AAIL,wDAAwD;AACxD,wBAAsB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6CjG;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,MAAM,GAAG,SAAS;;;;;;;;;;;;;;EAqD7E","sourcesContent":["/**\n * `history` 工具:一个工具、三种 type(list / read / search)。\n *\n * 用户裁决 2026-10-20:检索类能力合并为单工具、用类型与参数区分,避免工具过细。\n * 参数组合 fail-closed 校验;每类给出单行用法,模型不必逐个探索。\n */\nimport { Type } from \"typebox\";\nimport { filterSegmentsByScope, readSegmentSlice, readSegmentsFromLog, searchSegment } from \"./history-store.ts\";\n\n/**\n * Number of most recent segments the active index holds. Mirrors the value the\n * compaction policy uses (plugins must not import across package roots, so the\n * constant is restated here); the split is by recency only, so a mismatch would\n * shift the active/archive boundary but never lose data.\n */\nexport const HISTORY_ACTIVE_SEGMENTS = 10;\n\n/** Tool name exposed to the model. */\nexport const HISTORY_TOOL_NAME = \"history\";\n\nexport type HistoryType = \"list\" | \"read\" | \"search\";\n\n/** Parsed tool input after validation. */\nexport interface HistoryInput {\n\treadonly type: HistoryType;\n\treadonly scope: \"active\" | \"archive\";\n\treadonly segmentId?: string;\n\treadonly query?: string;\n\treadonly offset?: number;\n\treadonly limit?: number;\n}\n\n/**\n * Validate one tool call. Throws on any combination the schema cannot rule out\n * alone — `read` without an id, `search` without a query, out-of-range paging,\n * or parameters that belong to a different type.\n */\nexport function parseHistoryInput(raw: unknown): HistoryInput {\n\tif (typeof raw !== \"object\" || raw === null) throw new Error(\"history requires an object input\");\n\tconst input = raw as Record<string, unknown>;\n\tconst type = input.type;\n\tif (type !== \"list\" && type !== \"read\" && type !== \"search\") {\n\t\tthrow new Error('history.type must be \"list\", \"read\" or \"search\"');\n\t}\n\t// Optional fields accept JSON null as omitted: real models serialize absent\n\t// optional parameters as null rather than dropping the key (observed in the\n\t// 2026-10-24 S4 long-run retrieval: two real calls were rejected until the\n\t// model gave up). Null carries no value, so it is absence, not a value; the\n\t// cross-type and range checks below still fail closed on real conflicts.\n\tconst scope = input.scope === undefined || input.scope === null ? \"active\" : input.scope;\n\tif (scope !== \"active\" && scope !== \"archive\") throw new Error('history.scope must be \"active\" or \"archive\"');\n\n\tconst segmentId =\n\t\ttypeof input.segmentId === \"string\" && input.segmentId.trim() !== \"\" ? input.segmentId.trim() : undefined;\n\tconst query = typeof input.query === \"string\" && input.query.trim() !== \"\" ? input.query.trim() : undefined;\n\tconst offset = input.offset === undefined || input.offset === null ? undefined : input.offset;\n\tconst limit = input.limit === undefined || input.limit === null ? undefined : input.limit;\n\tif (offset !== undefined && (typeof offset !== \"number\" || !Number.isInteger(offset) || offset < 0)) {\n\t\tthrow new Error(\"history.offset must be a non-negative integer\");\n\t}\n\tif (limit !== undefined && (typeof limit !== \"number\" || !Number.isInteger(limit) || limit < 1)) {\n\t\tthrow new Error(\"history.limit must be a positive integer\");\n\t}\n\n\tif (type === \"list\") {\n\t\tif (segmentId !== undefined || query !== undefined || offset !== undefined || limit !== undefined) {\n\t\t\tthrow new Error('history type \"list\" accepts only scope; segmentId/query/offset/limit belong to read/search');\n\t\t}\n\t\treturn { type, scope };\n\t}\n\tif (type === \"read\") {\n\t\tif (segmentId === undefined) throw new Error('history type \"read\" requires segmentId');\n\t\tif (query !== undefined) throw new Error('history type \"read\" does not accept query; use type \"search\"');\n\t\treturn {\n\t\t\ttype,\n\t\t\tscope,\n\t\t\tsegmentId,\n\t\t\t...(offset === undefined ? {} : { offset }),\n\t\t\t...(limit === undefined ? {} : { limit }),\n\t\t};\n\t}\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tif (segmentId !== undefined || offset !== undefined || limit !== undefined) {\n\t\tthrow new Error('history type \"search\" accepts query and scope only; read a hit with type \"read\"');\n\t}\n\treturn { type, scope, query };\n}\n\n/** Index row returned by `list`. */\nexport interface HistoryListRow {\n\treadonly id: string;\n\treadonly closedAt: number;\n\treadonly summary: string;\n\treadonly summaryTokens: number;\n}\n\n/** Result shapes, kept small and JSON-serializable for the tool boundary. */\nexport type HistoryResult =\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"list\";\n\t\t\treadonly scope: string;\n\t\t\treadonly rows: readonly HistoryListRow[];\n\t\t\treadonly note?: string;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"read\";\n\t\t\treadonly segmentId: string;\n\t\t\treadonly lines: readonly string[];\n\t\t\treadonly truncated: boolean;\n\t\t\treadonly totalLines: number;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"search\";\n\t\t\treadonly query: string;\n\t\t\treadonly hits: readonly { readonly segmentId: string; readonly line: number; readonly text: string }[];\n\t };\n\nconst CHARS_PER_TOKEN = 3.5;\n\n/** Execute a validated call against one session log. */\nexport async function runHistory(input: HistoryInput, sessionFile: string): Promise<HistoryResult> {\n\tconst segments = await readSegmentsFromLog(sessionFile);\n\tconst scoped = filterSegmentsByScope(segments, input.scope, HISTORY_ACTIVE_SEGMENTS);\n\n\tif (input.type === \"list\") {\n\t\tconst rows = scoped.map((segment) => ({\n\t\t\tid: segment.id,\n\t\t\tclosedAt: segment.closedAt,\n\t\t\tsummary: segment.summary,\n\t\t\tsummaryTokens: Math.ceil(segment.summary.length / CHARS_PER_TOKEN),\n\t\t}));\n\t\treturn {\n\t\t\tok: true,\n\t\t\ttype: \"list\",\n\t\t\tscope: input.scope,\n\t\t\trows,\n\t\t\t...(scoped.length === 0 ? { note: \"no closed segments yet; the live conversation is still in context\" } : {}),\n\t\t};\n\t}\n\n\tif (input.type === \"read\") {\n\t\tconst segment = scoped.find((item) => item.id === input.segmentId);\n\t\tif (segment === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`segment ${input.segmentId} not found in scope \"${input.scope}\"; call history type=\"list\" to see available ids`,\n\t\t\t);\n\t\t}\n\t\tconst result = await readSegmentSlice(segment, {\n\t\t\tcurrentSessionFile: sessionFile,\n\t\t\t...(input.offset === undefined ? {} : { offset: input.offset }),\n\t\t\t...(input.limit === undefined ? {} : { limit: input.limit }),\n\t\t});\n\t\treturn { ok: true, type: \"read\", segmentId: segment.id, ...result };\n\t}\n\n\t// `type === \"search\"` guarantees a query at runtime; the union type does not\n\t// carry that, so narrow once here instead of asserting at each use.\n\tconst query = input.query;\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tconst hits: { segmentId: string; line: number; text: string }[] = [];\n\tfor (const segment of scoped) {\n\t\tconst found = await searchSegment(segment, query, { currentSessionFile: sessionFile });\n\t\tfor (const hit of found) hits.push({ segmentId: segment.id, line: hit.line, text: hit.text });\n\t}\n\treturn { ok: true, type: \"search\", query, hits };\n}\n\n/** The tool definition registered with the capability API. */\nexport function historyToolDefinition(getSessionFile: () => string | undefined) {\n\treturn {\n\t\tname: HISTORY_TOOL_NAME,\n\t\tlabel: \"Session history\",\n\t\tdescription: [\n\t\t\t'Read earlier conversation segments that were cut out of context to bound its size. type=\"list\" shows the index (ids, time, summary);',\n\t\t\t'type=\"read\" returns a segment\\'s original entries (segmentId required; offset/limit page through it);',\n\t\t\t'type=\"search\" greps inside segments (query required). scope=\"archive\" reaches older segments that left the index.',\n\t\t].join(\" \"),\n\t\tparameters: Type.Object({\n\t\t\ttype: Type.Union([Type.Literal(\"list\"), Type.Literal(\"read\"), Type.Literal(\"search\")], {\n\t\t\t\tdescription:\n\t\t\t\t\t\"list = index of segments; read = original entries of one segment; search = grep inside segments\",\n\t\t\t}),\n\t\t\tscope: Type.Optional(\n\t\t\t\tType.Union([Type.Literal(\"active\"), Type.Literal(\"archive\")], {\n\t\t\t\t\tdescription: \"active (default) = segments still in the index; archive = older segments\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tsegmentId: Type.Optional(\n\t\t\t\tType.String({ description: 'Segment id from type=\"list\" (required for type=\"read\")' }),\n\t\t\t),\n\t\t\tquery: Type.Optional(Type.String({ description: 'Text to find (required for type=\"search\")' })),\n\t\t\toffset: Type.Optional(\n\t\t\t\tType.Integer({ description: 'type=\"read\": skip this many lines (default 0)', minimum: 0 }),\n\t\t\t),\n\t\t\tlimit: Type.Optional(\n\t\t\t\tType.Integer({\n\t\t\t\t\tdescription: 'type=\"read\": return at most this many lines (default 200, max 2000)',\n\t\t\t\t\tminimum: 1,\n\t\t\t\t\tmaximum: 2000,\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\treadonlySafe: true,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t// The capability API passes either (input), (input, context) or\n\t\t\t// (context, input) depending on the host shape (goal/todo-set use the\n\t\t\t// same objectInput helper). The 2026-10-24 S4 long-run hit the\n\t\t\t// (context, input) shape: every history call failed with \"requires an\n\t\t\t// object input\" until the model gave up. Pick the object argument and\n\t\t\t// unwrap the context envelope's `input` field when present.\n\t\t\tconst candidate = typeof first === \"object\" && first !== null && !Array.isArray(first) ? first : second;\n\t\t\tconst raw =\n\t\t\t\ttypeof candidate === \"object\" && candidate !== null && \"input\" in (candidate as object)\n\t\t\t\t\t? (candidate as { input: unknown }).input\n\t\t\t\t\t: candidate;\n\t\t\tconst sessionFile = getSessionFile();\n\t\t\tif (sessionFile === undefined) throw new Error(\"this session has no file on disk; history is unavailable\");\n\t\t\tconst input = parseHistoryInput(raw);\n\t\t\treturn await runHistory(input, sessionFile);\n\t\t},\n\t};\n}\n"]}
|
package/dist/history-tool.js
CHANGED
|
@@ -136,8 +136,12 @@ export function historyToolDefinition(getSessionFile) {
|
|
|
136
136
|
})),
|
|
137
137
|
segmentId: Type.Optional(Type.String({ description: 'Segment id from type="list" (required for type="read")' })),
|
|
138
138
|
query: Type.Optional(Type.String({ description: 'Text to find (required for type="search")' })),
|
|
139
|
-
offset: Type.Optional(Type.
|
|
140
|
-
limit: Type.Optional(Type.
|
|
139
|
+
offset: Type.Optional(Type.Integer({ description: 'type="read": skip this many lines (default 0)', minimum: 0 })),
|
|
140
|
+
limit: Type.Optional(Type.Integer({
|
|
141
|
+
description: 'type="read": return at most this many lines (default 200, max 2000)',
|
|
142
|
+
minimum: 1,
|
|
143
|
+
maximum: 2000,
|
|
144
|
+
})),
|
|
141
145
|
}),
|
|
142
146
|
readonlySafe: true,
|
|
143
147
|
execute: async (first, second) => {
|
package/dist/history-tool.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history-tool.js","sourceRoot":"","sources":["../src/history-tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `history` 工具:一个工具、三种 type(list / read / search)。\n *\n * 用户裁决 2026-10-20:检索类能力合并为单工具、用类型与参数区分,避免工具过细。\n * 参数组合 fail-closed 校验;每类给出单行用法,模型不必逐个探索。\n */\nimport { Type } from \"typebox\";\nimport { filterSegmentsByScope, readSegmentSlice, readSegmentsFromLog, searchSegment } from \"./history-store.ts\";\n\n/**\n * Number of most recent segments the active index holds. Mirrors the value the\n * compaction policy uses (plugins must not import across package roots, so the\n * constant is restated here); the split is by recency only, so a mismatch would\n * shift the active/archive boundary but never lose data.\n */\nexport const HISTORY_ACTIVE_SEGMENTS = 10;\n\n/** Tool name exposed to the model. */\nexport const HISTORY_TOOL_NAME = \"history\";\n\nexport type HistoryType = \"list\" | \"read\" | \"search\";\n\n/** Parsed tool input after validation. */\nexport interface HistoryInput {\n\treadonly type: HistoryType;\n\treadonly scope: \"active\" | \"archive\";\n\treadonly segmentId?: string;\n\treadonly query?: string;\n\treadonly offset?: number;\n\treadonly limit?: number;\n}\n\n/**\n * Validate one tool call. Throws on any combination the schema cannot rule out\n * alone — `read` without an id, `search` without a query, out-of-range paging,\n * or parameters that belong to a different type.\n */\nexport function parseHistoryInput(raw: unknown): HistoryInput {\n\tif (typeof raw !== \"object\" || raw === null) throw new Error(\"history requires an object input\");\n\tconst input = raw as Record<string, unknown>;\n\tconst type = input.type;\n\tif (type !== \"list\" && type !== \"read\" && type !== \"search\") {\n\t\tthrow new Error('history.type must be \"list\", \"read\" or \"search\"');\n\t}\n\t// Optional fields accept JSON null as omitted: real models serialize absent\n\t// optional parameters as null rather than dropping the key (observed in the\n\t// 2026-10-24 S4 long-run retrieval: two real calls were rejected until the\n\t// model gave up). Null carries no value, so it is absence, not a value; the\n\t// cross-type and range checks below still fail closed on real conflicts.\n\tconst scope = input.scope === undefined || input.scope === null ? \"active\" : input.scope;\n\tif (scope !== \"active\" && scope !== \"archive\") throw new Error('history.scope must be \"active\" or \"archive\"');\n\n\tconst segmentId =\n\t\ttypeof input.segmentId === \"string\" && input.segmentId.trim() !== \"\" ? input.segmentId.trim() : undefined;\n\tconst query = typeof input.query === \"string\" && input.query.trim() !== \"\" ? input.query.trim() : undefined;\n\tconst offset = input.offset === undefined || input.offset === null ? undefined : input.offset;\n\tconst limit = input.limit === undefined || input.limit === null ? undefined : input.limit;\n\tif (offset !== undefined && (typeof offset !== \"number\" || !Number.isInteger(offset) || offset < 0)) {\n\t\tthrow new Error(\"history.offset must be a non-negative integer\");\n\t}\n\tif (limit !== undefined && (typeof limit !== \"number\" || !Number.isInteger(limit) || limit < 1)) {\n\t\tthrow new Error(\"history.limit must be a positive integer\");\n\t}\n\n\tif (type === \"list\") {\n\t\tif (segmentId !== undefined || query !== undefined || offset !== undefined || limit !== undefined) {\n\t\t\tthrow new Error('history type \"list\" accepts only scope; segmentId/query/offset/limit belong to read/search');\n\t\t}\n\t\treturn { type, scope };\n\t}\n\tif (type === \"read\") {\n\t\tif (segmentId === undefined) throw new Error('history type \"read\" requires segmentId');\n\t\tif (query !== undefined) throw new Error('history type \"read\" does not accept query; use type \"search\"');\n\t\treturn {\n\t\t\ttype,\n\t\t\tscope,\n\t\t\tsegmentId,\n\t\t\t...(offset === undefined ? {} : { offset }),\n\t\t\t...(limit === undefined ? {} : { limit }),\n\t\t};\n\t}\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tif (segmentId !== undefined || offset !== undefined || limit !== undefined) {\n\t\tthrow new Error('history type \"search\" accepts query and scope only; read a hit with type \"read\"');\n\t}\n\treturn { type, scope, query };\n}\n\n/** Index row returned by `list`. */\nexport interface HistoryListRow {\n\treadonly id: string;\n\treadonly closedAt: number;\n\treadonly summary: string;\n\treadonly summaryTokens: number;\n}\n\n/** Result shapes, kept small and JSON-serializable for the tool boundary. */\nexport type HistoryResult =\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"list\";\n\t\t\treadonly scope: string;\n\t\t\treadonly rows: readonly HistoryListRow[];\n\t\t\treadonly note?: string;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"read\";\n\t\t\treadonly segmentId: string;\n\t\t\treadonly lines: readonly string[];\n\t\t\treadonly truncated: boolean;\n\t\t\treadonly totalLines: number;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"search\";\n\t\t\treadonly query: string;\n\t\t\treadonly hits: readonly { readonly segmentId: string; readonly line: number; readonly text: string }[];\n\t };\n\nconst CHARS_PER_TOKEN = 3.5;\n\n/** Execute a validated call against one session log. */\nexport async function runHistory(input: HistoryInput, sessionFile: string): Promise<HistoryResult> {\n\tconst segments = await readSegmentsFromLog(sessionFile);\n\tconst scoped = filterSegmentsByScope(segments, input.scope, HISTORY_ACTIVE_SEGMENTS);\n\n\tif (input.type === \"list\") {\n\t\tconst rows = scoped.map((segment) => ({\n\t\t\tid: segment.id,\n\t\t\tclosedAt: segment.closedAt,\n\t\t\tsummary: segment.summary,\n\t\t\tsummaryTokens: Math.ceil(segment.summary.length / CHARS_PER_TOKEN),\n\t\t}));\n\t\treturn {\n\t\t\tok: true,\n\t\t\ttype: \"list\",\n\t\t\tscope: input.scope,\n\t\t\trows,\n\t\t\t...(scoped.length === 0 ? { note: \"no closed segments yet; the live conversation is still in context\" } : {}),\n\t\t};\n\t}\n\n\tif (input.type === \"read\") {\n\t\tconst segment = scoped.find((item) => item.id === input.segmentId);\n\t\tif (segment === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`segment ${input.segmentId} not found in scope \"${input.scope}\"; call history type=\"list\" to see available ids`,\n\t\t\t);\n\t\t}\n\t\tconst result = await readSegmentSlice(segment, {\n\t\t\tcurrentSessionFile: sessionFile,\n\t\t\t...(input.offset === undefined ? {} : { offset: input.offset }),\n\t\t\t...(input.limit === undefined ? {} : { limit: input.limit }),\n\t\t});\n\t\treturn { ok: true, type: \"read\", segmentId: segment.id, ...result };\n\t}\n\n\t// `type === \"search\"` guarantees a query at runtime; the union type does not\n\t// carry that, so narrow once here instead of asserting at each use.\n\tconst query = input.query;\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tconst hits: { segmentId: string; line: number; text: string }[] = [];\n\tfor (const segment of scoped) {\n\t\tconst found = await searchSegment(segment, query, { currentSessionFile: sessionFile });\n\t\tfor (const hit of found) hits.push({ segmentId: segment.id, line: hit.line, text: hit.text });\n\t}\n\treturn { ok: true, type: \"search\", query, hits };\n}\n\n/** The tool definition registered with the capability API. */\nexport function historyToolDefinition(getSessionFile: () => string | undefined) {\n\treturn {\n\t\tname: HISTORY_TOOL_NAME,\n\t\tlabel: \"Session history\",\n\t\tdescription: [\n\t\t\t'Read earlier conversation segments that were cut out of context to bound its size. type=\"list\" shows the index (ids, time, summary);',\n\t\t\t'type=\"read\" returns a segment\\'s original entries (segmentId required; offset/limit page through it);',\n\t\t\t'type=\"search\" greps inside segments (query required). scope=\"archive\" reaches older segments that left the index.',\n\t\t].join(\" \"),\n\t\tparameters: Type.Object({\n\t\t\ttype: Type.Union([Type.Literal(\"list\"), Type.Literal(\"read\"), Type.Literal(\"search\")], {\n\t\t\t\tdescription:\n\t\t\t\t\t\"list = index of segments; read = original entries of one segment; search = grep inside segments\",\n\t\t\t}),\n\t\t\tscope: Type.Optional(\n\t\t\t\tType.Union([Type.Literal(\"active\"), Type.Literal(\"archive\")], {\n\t\t\t\t\tdescription: \"active (default) = segments still in the index; archive = older segments\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tsegmentId: Type.Optional(\n\t\t\t\tType.String({ description: 'Segment id from type=\"list\" (required for type=\"read\")' }),\n\t\t\t),\n\t\t\tquery: Type.Optional(Type.String({ description: 'Text to find (required for type=\"search\")' })),\n\t\t\toffset: Type.Optional(Type.Number({ description: 'type=\"read\": skip this many lines (default 0)' })),\n\t\t\tlimit: Type.Optional(\n\t\t\t\tType.Number({ description: 'type=\"read\": return at most this many lines (default 200)' }),\n\t\t\t),\n\t\t}),\n\t\treadonlySafe: true,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t// The capability API passes either (input), (input, context) or\n\t\t\t// (context, input) depending on the host shape (goal/todo-set use the\n\t\t\t// same objectInput helper). The 2026-10-24 S4 long-run hit the\n\t\t\t// (context, input) shape: every history call failed with \"requires an\n\t\t\t// object input\" until the model gave up. Pick the object argument and\n\t\t\t// unwrap the context envelope's `input` field when present.\n\t\t\tconst candidate = typeof first === \"object\" && first !== null && !Array.isArray(first) ? first : second;\n\t\t\tconst raw =\n\t\t\t\ttypeof candidate === \"object\" && candidate !== null && \"input\" in (candidate as object)\n\t\t\t\t\t? (candidate as { input: unknown }).input\n\t\t\t\t\t: candidate;\n\t\t\tconst sessionFile = getSessionFile();\n\t\t\tif (sessionFile === undefined) throw new Error(\"this session has no file on disk; history is unavailable\");\n\t\t\tconst input = parseHistoryInput(raw);\n\t\t\treturn await runHistory(input, sessionFile);\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"history-tool.js","sourceRoot":"","sources":["../src/history-tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `history` 工具:一个工具、三种 type(list / read / search)。\n *\n * 用户裁决 2026-10-20:检索类能力合并为单工具、用类型与参数区分,避免工具过细。\n * 参数组合 fail-closed 校验;每类给出单行用法,模型不必逐个探索。\n */\nimport { Type } from \"typebox\";\nimport { filterSegmentsByScope, readSegmentSlice, readSegmentsFromLog, searchSegment } from \"./history-store.ts\";\n\n/**\n * Number of most recent segments the active index holds. Mirrors the value the\n * compaction policy uses (plugins must not import across package roots, so the\n * constant is restated here); the split is by recency only, so a mismatch would\n * shift the active/archive boundary but never lose data.\n */\nexport const HISTORY_ACTIVE_SEGMENTS = 10;\n\n/** Tool name exposed to the model. */\nexport const HISTORY_TOOL_NAME = \"history\";\n\nexport type HistoryType = \"list\" | \"read\" | \"search\";\n\n/** Parsed tool input after validation. */\nexport interface HistoryInput {\n\treadonly type: HistoryType;\n\treadonly scope: \"active\" | \"archive\";\n\treadonly segmentId?: string;\n\treadonly query?: string;\n\treadonly offset?: number;\n\treadonly limit?: number;\n}\n\n/**\n * Validate one tool call. Throws on any combination the schema cannot rule out\n * alone — `read` without an id, `search` without a query, out-of-range paging,\n * or parameters that belong to a different type.\n */\nexport function parseHistoryInput(raw: unknown): HistoryInput {\n\tif (typeof raw !== \"object\" || raw === null) throw new Error(\"history requires an object input\");\n\tconst input = raw as Record<string, unknown>;\n\tconst type = input.type;\n\tif (type !== \"list\" && type !== \"read\" && type !== \"search\") {\n\t\tthrow new Error('history.type must be \"list\", \"read\" or \"search\"');\n\t}\n\t// Optional fields accept JSON null as omitted: real models serialize absent\n\t// optional parameters as null rather than dropping the key (observed in the\n\t// 2026-10-24 S4 long-run retrieval: two real calls were rejected until the\n\t// model gave up). Null carries no value, so it is absence, not a value; the\n\t// cross-type and range checks below still fail closed on real conflicts.\n\tconst scope = input.scope === undefined || input.scope === null ? \"active\" : input.scope;\n\tif (scope !== \"active\" && scope !== \"archive\") throw new Error('history.scope must be \"active\" or \"archive\"');\n\n\tconst segmentId =\n\t\ttypeof input.segmentId === \"string\" && input.segmentId.trim() !== \"\" ? input.segmentId.trim() : undefined;\n\tconst query = typeof input.query === \"string\" && input.query.trim() !== \"\" ? input.query.trim() : undefined;\n\tconst offset = input.offset === undefined || input.offset === null ? undefined : input.offset;\n\tconst limit = input.limit === undefined || input.limit === null ? undefined : input.limit;\n\tif (offset !== undefined && (typeof offset !== \"number\" || !Number.isInteger(offset) || offset < 0)) {\n\t\tthrow new Error(\"history.offset must be a non-negative integer\");\n\t}\n\tif (limit !== undefined && (typeof limit !== \"number\" || !Number.isInteger(limit) || limit < 1)) {\n\t\tthrow new Error(\"history.limit must be a positive integer\");\n\t}\n\n\tif (type === \"list\") {\n\t\tif (segmentId !== undefined || query !== undefined || offset !== undefined || limit !== undefined) {\n\t\t\tthrow new Error('history type \"list\" accepts only scope; segmentId/query/offset/limit belong to read/search');\n\t\t}\n\t\treturn { type, scope };\n\t}\n\tif (type === \"read\") {\n\t\tif (segmentId === undefined) throw new Error('history type \"read\" requires segmentId');\n\t\tif (query !== undefined) throw new Error('history type \"read\" does not accept query; use type \"search\"');\n\t\treturn {\n\t\t\ttype,\n\t\t\tscope,\n\t\t\tsegmentId,\n\t\t\t...(offset === undefined ? {} : { offset }),\n\t\t\t...(limit === undefined ? {} : { limit }),\n\t\t};\n\t}\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tif (segmentId !== undefined || offset !== undefined || limit !== undefined) {\n\t\tthrow new Error('history type \"search\" accepts query and scope only; read a hit with type \"read\"');\n\t}\n\treturn { type, scope, query };\n}\n\n/** Index row returned by `list`. */\nexport interface HistoryListRow {\n\treadonly id: string;\n\treadonly closedAt: number;\n\treadonly summary: string;\n\treadonly summaryTokens: number;\n}\n\n/** Result shapes, kept small and JSON-serializable for the tool boundary. */\nexport type HistoryResult =\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"list\";\n\t\t\treadonly scope: string;\n\t\t\treadonly rows: readonly HistoryListRow[];\n\t\t\treadonly note?: string;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"read\";\n\t\t\treadonly segmentId: string;\n\t\t\treadonly lines: readonly string[];\n\t\t\treadonly truncated: boolean;\n\t\t\treadonly totalLines: number;\n\t }\n\t| {\n\t\t\treadonly ok: true;\n\t\t\treadonly type: \"search\";\n\t\t\treadonly query: string;\n\t\t\treadonly hits: readonly { readonly segmentId: string; readonly line: number; readonly text: string }[];\n\t };\n\nconst CHARS_PER_TOKEN = 3.5;\n\n/** Execute a validated call against one session log. */\nexport async function runHistory(input: HistoryInput, sessionFile: string): Promise<HistoryResult> {\n\tconst segments = await readSegmentsFromLog(sessionFile);\n\tconst scoped = filterSegmentsByScope(segments, input.scope, HISTORY_ACTIVE_SEGMENTS);\n\n\tif (input.type === \"list\") {\n\t\tconst rows = scoped.map((segment) => ({\n\t\t\tid: segment.id,\n\t\t\tclosedAt: segment.closedAt,\n\t\t\tsummary: segment.summary,\n\t\t\tsummaryTokens: Math.ceil(segment.summary.length / CHARS_PER_TOKEN),\n\t\t}));\n\t\treturn {\n\t\t\tok: true,\n\t\t\ttype: \"list\",\n\t\t\tscope: input.scope,\n\t\t\trows,\n\t\t\t...(scoped.length === 0 ? { note: \"no closed segments yet; the live conversation is still in context\" } : {}),\n\t\t};\n\t}\n\n\tif (input.type === \"read\") {\n\t\tconst segment = scoped.find((item) => item.id === input.segmentId);\n\t\tif (segment === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`segment ${input.segmentId} not found in scope \"${input.scope}\"; call history type=\"list\" to see available ids`,\n\t\t\t);\n\t\t}\n\t\tconst result = await readSegmentSlice(segment, {\n\t\t\tcurrentSessionFile: sessionFile,\n\t\t\t...(input.offset === undefined ? {} : { offset: input.offset }),\n\t\t\t...(input.limit === undefined ? {} : { limit: input.limit }),\n\t\t});\n\t\treturn { ok: true, type: \"read\", segmentId: segment.id, ...result };\n\t}\n\n\t// `type === \"search\"` guarantees a query at runtime; the union type does not\n\t// carry that, so narrow once here instead of asserting at each use.\n\tconst query = input.query;\n\tif (query === undefined) throw new Error('history type \"search\" requires query');\n\tconst hits: { segmentId: string; line: number; text: string }[] = [];\n\tfor (const segment of scoped) {\n\t\tconst found = await searchSegment(segment, query, { currentSessionFile: sessionFile });\n\t\tfor (const hit of found) hits.push({ segmentId: segment.id, line: hit.line, text: hit.text });\n\t}\n\treturn { ok: true, type: \"search\", query, hits };\n}\n\n/** The tool definition registered with the capability API. */\nexport function historyToolDefinition(getSessionFile: () => string | undefined) {\n\treturn {\n\t\tname: HISTORY_TOOL_NAME,\n\t\tlabel: \"Session history\",\n\t\tdescription: [\n\t\t\t'Read earlier conversation segments that were cut out of context to bound its size. type=\"list\" shows the index (ids, time, summary);',\n\t\t\t'type=\"read\" returns a segment\\'s original entries (segmentId required; offset/limit page through it);',\n\t\t\t'type=\"search\" greps inside segments (query required). scope=\"archive\" reaches older segments that left the index.',\n\t\t].join(\" \"),\n\t\tparameters: Type.Object({\n\t\t\ttype: Type.Union([Type.Literal(\"list\"), Type.Literal(\"read\"), Type.Literal(\"search\")], {\n\t\t\t\tdescription:\n\t\t\t\t\t\"list = index of segments; read = original entries of one segment; search = grep inside segments\",\n\t\t\t}),\n\t\t\tscope: Type.Optional(\n\t\t\t\tType.Union([Type.Literal(\"active\"), Type.Literal(\"archive\")], {\n\t\t\t\t\tdescription: \"active (default) = segments still in the index; archive = older segments\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tsegmentId: Type.Optional(\n\t\t\t\tType.String({ description: 'Segment id from type=\"list\" (required for type=\"read\")' }),\n\t\t\t),\n\t\t\tquery: Type.Optional(Type.String({ description: 'Text to find (required for type=\"search\")' })),\n\t\t\toffset: Type.Optional(\n\t\t\t\tType.Integer({ description: 'type=\"read\": skip this many lines (default 0)', minimum: 0 }),\n\t\t\t),\n\t\t\tlimit: Type.Optional(\n\t\t\t\tType.Integer({\n\t\t\t\t\tdescription: 'type=\"read\": return at most this many lines (default 200, max 2000)',\n\t\t\t\t\tminimum: 1,\n\t\t\t\t\tmaximum: 2000,\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t\treadonlySafe: true,\n\t\texecute: async (first: unknown, second?: unknown) => {\n\t\t\t// The capability API passes either (input), (input, context) or\n\t\t\t// (context, input) depending on the host shape (goal/todo-set use the\n\t\t\t// same objectInput helper). The 2026-10-24 S4 long-run hit the\n\t\t\t// (context, input) shape: every history call failed with \"requires an\n\t\t\t// object input\" until the model gave up. Pick the object argument and\n\t\t\t// unwrap the context envelope's `input` field when present.\n\t\t\tconst candidate = typeof first === \"object\" && first !== null && !Array.isArray(first) ? first : second;\n\t\t\tconst raw =\n\t\t\t\ttypeof candidate === \"object\" && candidate !== null && \"input\" in (candidate as object)\n\t\t\t\t\t? (candidate as { input: unknown }).input\n\t\t\t\t\t: candidate;\n\t\t\tconst sessionFile = getSessionFile();\n\t\t\tif (sessionFile === undefined) throw new Error(\"this session has no file on disk; history is unavailable\");\n\t\t\tconst input = parseHistoryInput(raw);\n\t\t\treturn await runHistory(input, sessionFile);\n\t\t},\n\t};\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-agent-forge/plugin-session-history",
|
|
3
|
-
"version": "0.88.
|
|
3
|
+
"version": "0.88.20",
|
|
4
4
|
"description": "Session history retrieval for Agent Forge: one `history` tool (list/read/search) over closed conversation segments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/entry.js",
|