@astrosheep/pi-context 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +52 -5
  2. package/dist/build-info.json +4 -0
  3. package/dist/extension.js +1861 -0
  4. package/dist/src/context/budget.js +150 -0
  5. package/dist/src/context/context-window.js +97 -0
  6. package/dist/src/context/prompts.js +94 -0
  7. package/dist/src/context/reset-lifecycle.js +134 -0
  8. package/dist/src/context/runtime.js +236 -0
  9. package/dist/src/context/thresholds.js +62 -0
  10. package/dist/src/dream/cli.js +1 -1
  11. package/dist/src/dream/doctor.js +34 -6
  12. package/dist/src/dream/runner.js +1 -1
  13. package/dist/src/dream/settings.js +30 -0
  14. package/dist/src/{history-tools.js → history/history-tools.js} +3 -3
  15. package/dist/src/{history.js → history/history.js} +8 -46
  16. package/dist/src/index.js +27 -94
  17. package/dist/src/notes/address.js +97 -16
  18. package/dist/src/notes/frontmatter.js +18 -3
  19. package/dist/src/notes/notes-snapshot.js +30 -0
  20. package/dist/src/notes/paths.js +64 -7
  21. package/dist/src/notes/session-replay.js +41 -0
  22. package/dist/src/notes/store.js +76 -22
  23. package/dist/src/notes/tools.js +7 -7
  24. package/dist/src/protocol.js +7 -5
  25. package/dist/src/settings.js +16 -0
  26. package/dist/src/tool-schema.js +1 -1
  27. package/dist/test/agent-loop.test.js +813 -221
  28. package/dist/test/boot.integration.test.js +167 -0
  29. package/dist/test/budget-settings.integration.test.js +126 -0
  30. package/dist/test/doctor.test.js +14 -36
  31. package/dist/test/dream.test.js +37 -380
  32. package/dist/test/helpers/extension.js +393 -0
  33. package/dist/test/history.integration.test.js +316 -0
  34. package/dist/test/notes.integration.test.js +273 -0
  35. package/dist/test/notes.test.js +40 -359
  36. package/dist/test/reset-lifecycle.test.js +248 -180
  37. package/docs/architecture.md +35 -18
  38. package/docs/reset-lifecycle.md +16 -14
  39. package/package.json +11 -10
  40. package/src/context/budget.ts +148 -0
  41. package/src/context/context-window.ts +103 -0
  42. package/src/context/prompts.ts +111 -0
  43. package/src/context/reset-lifecycle.ts +145 -0
  44. package/src/context/runtime.ts +246 -0
  45. package/src/context/thresholds.ts +78 -0
  46. package/src/dream/cli.ts +1 -1
  47. package/src/dream/doctor.ts +27 -6
  48. package/src/dream/runner.ts +1 -1
  49. package/src/dream/settings.ts +32 -0
  50. package/src/{history-tools.ts → history/history-tools.ts} +3 -3
  51. package/src/{history.ts → history/history.ts} +9 -48
  52. package/src/index.ts +27 -89
  53. package/src/notes/address.ts +82 -16
  54. package/src/notes/frontmatter.ts +20 -3
  55. package/src/notes/notes-snapshot.ts +40 -0
  56. package/src/notes/paths.ts +64 -7
  57. package/src/notes/session-replay.ts +53 -0
  58. package/src/notes/store.ts +78 -25
  59. package/src/notes/tools.ts +7 -7
  60. package/src/protocol.ts +7 -5
  61. package/src/settings.ts +20 -0
  62. package/src/tool-schema.ts +1 -2
  63. package/dist/src/budget.js +0 -65
  64. package/dist/src/notes/model.js +0 -101
  65. package/dist/src/prompts.js +0 -88
  66. package/dist/src/reset-lifecycle.js +0 -155
  67. package/dist/src/thresholds.js +0 -102
  68. package/dist/src/warning.js +0 -44
  69. package/dist/test/coherence.test.js +0 -371
  70. package/dist/test/history.test.js +0 -26
  71. package/dist/test/integration.test.js +0 -1759
  72. package/dist/test/pagination.property.test.js +0 -471
  73. package/src/budget.ts +0 -67
  74. package/src/notes/model.ts +0 -109
  75. package/src/prompts.ts +0 -91
  76. package/src/reset-lifecycle.ts +0 -173
  77. package/src/thresholds.ts +0 -110
  78. package/src/warning.ts +0 -46
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@earendil-works/pi-ai";
2
2
  import { defineTool } from "@earendil-works/pi-coding-agent";
3
- import { localIso } from "./model.js";
3
+ import { localIso } from "./frontmatter.js";
4
4
  import { DEFAULT_READ_WINDOW_CHARS, MAX_READ_WINDOW_CHARS, middleTruncate, output, outputRaw, page, prefixFit, readCharacterWindow, readWindowBlock, withinTextBudget } from "../tool-output.js";
5
5
  import { cursor, nullableString, positiveInteger, searchQueries, searchQuery } from "../tool-schema.js";
6
6
  import { assertAddress } from "./address.js";
@@ -8,7 +8,7 @@ import { NoteError, editNote, listNotes, readNote, searchNotes, writeNote } from
8
8
  const ORIGIN = Type.Optional(Type.Union([Type.Literal("user"), Type.Literal("self"), Type.Literal("external")], {
9
9
  description: "Where the note's content came from. user: written or dictated by the human. self: written by you, the agent (default). external: anything else — third-party text, tool output, fetched material.",
10
10
  }));
11
- const ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session, `@project/<vpath>` for this project's home, and `@personal/<vpath>` for the human's cross-project home. `@` means leaving home. Any other `@` prefix, or `@` inside a vpath, is a hard error: legal prefixes are `@project/` and `@personal/`; bare names are the session home. There is no cross-home fallback. Paths reject `..`, absolute paths, and backslashes.";
11
+ const ADDRESS_DESCRIPTION = "Address forms are bare `<vpath>` for this session, `@project/<vpath>` for this project, `@human/<vpath>` for the human's cross-project home, `@self/<vpath>` / `@agents/<name>/<vpath>` for agent homes, and `@model/<vpath>` / `@models/<name>/<vpath>` for model homes. `@self` and `@model` mean the current agent/model; the `<name>` forms name one absolutely. The word after `@` is always one of the reserved home names — names live at the second level, never `@faye/`. `@` means leaving home. Any other `@` prefix, or `@` inside a vpath, is a hard error. There is no cross-home fallback. Paths reject `..`, absolute paths, and backslashes. Homes you do not own (`@agents/<other>/`, `@models/<other>/`) are read-only.";
12
12
  function failure(error) {
13
13
  if (error instanceof NoteError) {
14
14
  const payload = { error: error.message };
@@ -29,7 +29,7 @@ export function registerNotesTools(pi) {
29
29
  const content = params.content;
30
30
  try {
31
31
  const destination = assertAddress(params.address);
32
- writeNote(ctx, destination.path, content, { scope: destination.scope, origin: (params.origin ?? "self"), stale: params.stale });
32
+ writeNote(ctx, destination.path, content, { scope: destination.scope, who: destination.who, origin: (params.origin ?? "self"), stale: params.stale });
33
33
  return output({ address: params.address, written: true });
34
34
  }
35
35
  catch (error) {
@@ -44,7 +44,7 @@ export function registerNotesTools(pi) {
44
44
  async execute(_id, params, _signal, _update, ctx) {
45
45
  try {
46
46
  const destination = assertAddress(params.address);
47
- const { applied, diff } = editNote(ctx, destination.path, destination.scope, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all });
47
+ const { applied, diff } = editNote(ctx, destination.path, destination.scope, params.edits, { origin: params.origin, stale: params.stale, replaceAll: params.replace_all }, destination.who);
48
48
  return output({ address: params.address, applied, diff });
49
49
  }
50
50
  catch (error) {
@@ -60,7 +60,7 @@ export function registerNotesTools(pi) {
60
60
  let note;
61
61
  try {
62
62
  const destination = assertAddress(params.address);
63
- note = readNote(ctx, destination.path, destination.scope);
63
+ note = readNote(ctx, destination.path, destination.scope, destination.who);
64
64
  }
65
65
  catch (error) {
66
66
  return failure(error);
@@ -79,7 +79,7 @@ export function registerNotesTools(pi) {
79
79
  }));
80
80
  pi.registerTool(defineTool({
81
81
  name: "notes_list", label: "Notes list",
82
- description: `List note files as rows carrying address, updated_at, and stale, most recently updated first. ${ADDRESS_DESCRIPTION} All three homes are merged. A glob pattern (* within a path segment, ** across segments) filters full address strings: *.md is session-only, @project/** is project-only, and ** covers every home.`,
82
+ description: `List note files as rows carrying address, updated_at, and stale, most recently updated first. ${ADDRESS_DESCRIPTION} Listings merge your five reachable homes: this session, @project/, @human/, your @self home, and the current @model home; other agents and models appear only under an explicit glob (@agents/<name>/**, @models/<name>/**, or a glob in the name segment to scan a whole namespace).`,
83
83
  parameters: Type.Object({ pattern: nullableString(), cursor: cursor(), max_results: positiveInteger() }, { additionalProperties: false }),
84
84
  async execute(_id, params, _signal, _update, ctx) {
85
85
  let rows;
@@ -100,7 +100,7 @@ export function registerNotesTools(pi) {
100
100
  }));
101
101
  pi.registerTool(defineTool({
102
102
  name: "notes_search", label: "Notes search",
103
- description: `Case-sensitive literal substring search over note bodies; query is one string or several (OR), each matched line appears once. ${ADDRESS_DESCRIPTION} All three homes are merged and every entry carries its full address. Patterns glob over full address strings. Each file entry carries matches_total, its full match count before capping. Each match carries line, text, offset_chars (a code-point offset into the serialized note returned by notes_read, at the earliest query match), and truncated.`,
103
+ description: `Case-sensitive literal substring search over note bodies; query is one string or several (OR), each matched line appears once. ${ADDRESS_DESCRIPTION} Search merges the same five reachable homes as notes_list; explicit globs reach other agents and models. Patterns glob over full address strings. Each file entry carries matches_total, its full match count before capping. Each match carries line, text, offset_chars (a code-point offset into the serialized note returned by notes_read, at the earliest query match), and truncated.`,
104
104
  parameters: Type.Object({ query: searchQuery(), pattern: nullableString(), cursor: cursor(), max_matches_per_file: positiveInteger(), max_files: positiveInteger() }, { additionalProperties: false }),
105
105
  async execute(_id, params, _signal, _update, ctx) {
106
106
  const queries = searchQueries(params.query);
@@ -1,15 +1,15 @@
1
- export const STATE_TYPE = "pi-context/state";
2
1
  export const NOTE_TYPE = "pi-context/note";
3
2
  export const BOOT_TYPE = "pi-context/boot";
4
3
  export const GUIDANCE_TYPE = "pi-context/guidance";
5
4
  export const WARNING_TYPE = "pi-context/warning";
6
5
  export const RESET_MARKER_TYPE = "pi-context/reset-marker";
7
6
  export const CONTINUATION_TYPE = "pi-context/continuation";
8
- export const RESET_V2 = "reset-v2";
9
7
  export const MAX_NOTE_BYTES = 1_000_000;
10
8
  export const POCKET_SESSION_LIMIT = 5;
11
9
  export const POCKET_PROJECT_LIMIT = 2;
12
- export const POCKET_PERSONAL_LIMIT = 2;
10
+ export const POCKET_HUMAN_LIMIT = 2;
11
+ export const POCKET_AGENT_LIMIT = 1;
12
+ export const POCKET_MODEL_LIMIT = 1;
13
13
  // Write-time cap on a virtual note path. Deliberately NOT enforced by assertVirtualPath:
14
14
  // notesFromSession replays already-persisted operations, which must keep loading sessions
15
15
  // that contain a longer legacy path. Reads and replay stay un-capped.
@@ -49,9 +49,11 @@ Use get_context_remaining to see how much of the window is left. When it runs ou
49
49
 
50
50
  If <context_window> lists a Previous context window id, a reset just happened and the old conversation is not included. Read your note checkpoint first, then recover details through history_*: history_read directly when you know the window and item IDs, history_list or history_search to find them when you don't.
51
51
 
52
- Your notes live in three homes: this session (bare names), this project (@project/<vpath>), the human across projects (@personal/<vpath>). @ means leaving home — and homes don't visit each other: there is no cross-home fallback.
52
+ Notes live in five homes, and the word after @ is always one of their reserved names — your own name and other people's names live at the second level (@agents/faye/, never @faye/). Bare names are this session; @project/<vpath> is this project's workspace; @human/<vpath> is the human's cross-project home; @self/<vpath> and @agents/<name>/<vpath> are agent homes; @model/<vpath> and @models/<name>/<vpath> are model homes. @self and @model are the only relative forms the current agent, the current model — and listings never show them, only the resolved name. There is no cross-home fallback.
53
53
  Session notes belong to this trip — the goal, the progress, the loose ends. The next window of THIS trip wakes to them; once the trip is over, nobody does.
54
54
  @project notes hold facts about this project — architecture, conventions, workflows, deployment and environment details — for whoever works here next.
55
- @personal notes hold the human's durable preferences and standing rules, plus lessons that apply across projects. Duration does not make a note personal; its stated scope must already be broader than the project or conversation at hand. When the intended scope is unclear, keep the note in the narrowest stated scope rather than widening it.
55
+ @human notes hold the human's durable preferences and standing rules, plus lessons that apply across projects for every agent that serves this human, whoever is running. You write there as the human's scribe; what the human dictates carries origin: user. When the intended scope is unclear, keep the note in the narrowest stated scope rather than widening it.
56
+ @self notes are yours — your voice, your lessons, your gripes — for the next run of whoever you are. Other agents read yours by explicit address and never write them; you read theirs the same way. A note only its author would ever need belongs here, not in @human.
57
+ @model notes capture the substrate — how the current model actually behaves: context honesty, tool quirks, fallback patterns. @model resolves live, so what you learn on one model is filed under that model even when a fallback moves you mid-window.
56
58
  ${CONTEXT_WINDOW_PROTOCOL_CLOSE_TAG}`;
57
59
  export const WARNING_PROMPT = "Your memory is about to be erased. Write the note. NOW. If it already exists, revise it with notes_edit (or rewrite it whole): the goal, decisions, progress, learnings, next steps, the skills you still need, the window ID and item ID of every relevant user request still being solved, and important actions/tool calls for future reference. Do not continue any task. Then call wipe_memory IMMEDIATELY — anything not in the note dies with the window.";
@@ -0,0 +1,16 @@
1
+ import { PI_CONTEXT_SETTINGS_KEY } from "./protocol.js";
2
+ function isSettingsObject(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ /** Read the raw "pi-context" object from one parsed settings scope. */
6
+ function piContextSettings(settings) {
7
+ if (!isSettingsObject(settings))
8
+ return {};
9
+ const value = settings[PI_CONTEXT_SETTINGS_KEY];
10
+ return isSettingsObject(value) ? value : {};
11
+ }
12
+ /** Merge the global and project "pi-context" objects per key; project wins, mirroring Pi's deep merge. */
13
+ export function mergePiContextSettings(globalSettings, projectSettings) {
14
+ const merged = { ...piContextSettings(globalSettings), ...piContextSettings(projectSettings) };
15
+ return { reminderMarginTokens: merged.reminderMarginTokens, dreamer: merged.dreamer };
16
+ }
@@ -4,7 +4,7 @@ export const positiveInteger = () => Type.Optional(Type.Integer({ minimum: 1 }))
4
4
  export const cursor = () => Type.Optional(Type.Integer({ minimum: 0, description: "Continuation cursor: pass the previous next_cursor back unchanged, with the same filters and ordering. Omit to start. next_cursor is null only when the set is exhausted." }));
5
5
  export const recentFirst = () => Type.Optional(Type.Boolean({ description: "Return newest-first. Only an explicit false returns oldest-first. Defaults to true." }));
6
6
  /** Role filter. `developer` is the known author for this extension's own custom entries. */
7
- export const role = Type.Union([Type.Literal("user"), Type.Literal("assistant"), Type.Literal("tool_call"), Type.Literal("tool"), Type.Literal("system"), Type.Literal("developer"), Type.Null()], { description: "Filter by the item's role. Exactly six: \"user\" and \"assistant\" are a message's visible text (assistant text never contains tool calls); \"tool_call\" is one tool invocation (tool_name set, content = the call's JSON arguments); \"tool\" is one tool run's output (tool_name set); \"system\" is a native Pi compaction summary; \"developer\" is an entry this extension authored (boot, guidance, warning, continuation messages, reset-window compaction summaries, any pi-context/* entry)." });
7
+ export const role = Type.Union([Type.Literal("user"), Type.Literal("assistant"), Type.Literal("tool_call"), Type.Literal("tool"), Type.Literal("system"), Type.Literal("developer"), Type.Null()], { description: "Filter by the item's role. Exactly six: \"user\" and \"assistant\" are a message's visible text (assistant text never contains tool calls); \"tool_call\" is one tool invocation (tool_name set, content = the call's JSON arguments); \"tool\" is one tool run's output (tool_name set); \"system\" is a native Pi compaction or branch summary; \"developer\" is an entry this extension authored (boot, guidance, warning, continuation messages, or any pi-context/* custom message)." });
8
8
  /** Search query parameter: one literal, or several literals combined with OR. */
9
9
  export const searchQuery = () => Type.Union([Type.String(), Type.Array(Type.String(), { minItems: 1 })]);
10
10
  /**