neoctl 0.1.2 → 0.1.4
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/README.md +151 -151
- package/dist/context/compaction.d.ts +10 -1
- package/dist/context/compaction.js +161 -1
- package/dist/context/compaction.js.map +1 -1
- package/dist/context/prompts.js +1 -0
- package/dist/context/prompts.js.map +1 -1
- package/dist/core/query-engine.d.ts +11 -3
- package/dist/core/query-engine.js +60 -0
- package/dist/core/query-engine.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/repl/commands.d.ts +4 -0
- package/dist/repl/commands.js +6 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +118 -6
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/markdown-renderer.js +3 -1
- package/dist/repl/markdown-renderer.js.map +1 -1
- package/dist/session/session-store.d.ts +6 -0
- package/dist/session/session-store.js +9 -0
- package/dist/session/session-store.js.map +1 -1
- package/dist/tools/builtins/plan-tool.d.ts +19 -0
- package/dist/tools/builtins/plan-tool.js +66 -0
- package/dist/tools/builtins/plan-tool.js.map +1 -0
- package/dist/tools/smoke-tool-system.js +22 -1
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
# Agent Scaffold Source
|
|
2
|
-
|
|
3
|
-
This directory is a TypeScript implementation scaffold for the parent README. Chapters 01, 02, 03, 04, 05-core, and 07 now have runnable paths; later chapters still expose stable module boundaries and placeholders.
|
|
4
|
-
|
|
5
|
-
## Shape
|
|
6
|
-
|
|
7
|
-
- `src/repl`: the UI layer. It owns terminal input, slash commands, system init events, and rendering streamed events.
|
|
8
|
-
- `src/core`: the multi-turn query loop, loop state, message pipeline, `QueryEngine`, and child-agent runner entry points.
|
|
9
|
-
- `src/model`: provider-neutral model gateway/config/factory, provider adapters, OpenAI Responses/Chat mappers, HTTP transport, SSE decoder, retry, and normalized errors.
|
|
10
|
-
- `src/tools`: lifecycle tool contracts, registry, schema validation, execution pipeline, batch orchestration, streaming executor, and built-in tools.
|
|
11
|
-
- `src/context`: system prompt sections, runtime user/system context, deterministic compaction helpers, and model-driven compaction.
|
|
12
|
-
- `src/session`: JSONL session transcripts, tool-result persistence, latest/specific session resume, and session listing.
|
|
13
|
-
- `src/agents`: agent definitions, `AgentTool`, prompt rules, fork constraints, local task lifecycle, and agent smoke coverage.
|
|
14
|
-
- `src/tasks`: background task store, task-control tools, named-agent message routing, and persisted task output files.
|
|
15
|
-
- `src/skills`: inline workflow-as-tool injection and fork-skill boundary.
|
|
16
|
-
- `src/safety`: optional permission, sandbox, and audit ports.
|
|
17
|
-
- `src/app`: app-state ports used by tools and runtime code.
|
|
18
|
-
- `vendor/ripgrep`: per-platform bundled `rg` binaries installed by `npm run vendor:rg` or optional `postinstall`.
|
|
19
|
-
|
|
20
|
-
## Commands
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install
|
|
24
|
-
npm run vendor:rg
|
|
25
|
-
npm run typecheck
|
|
26
|
-
npm run build
|
|
27
|
-
npm run smoke:core
|
|
28
|
-
npm run smoke:tools
|
|
29
|
-
npm run smoke:context
|
|
30
|
-
npm run smoke:agents
|
|
31
|
-
npm run smoke:skills
|
|
32
|
-
npm run smoke:openai -- "Say pong"
|
|
33
|
-
npm run dev
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Core Loop
|
|
37
|
-
|
|
38
|
-
`src/core/query.ts` implements the Chapter 01 main path as a streaming state machine:
|
|
39
|
-
|
|
40
|
-
- prepares each turn from a single `QueryState`
|
|
41
|
-
- applies compact-boundary filtering and tool-result budgeting before model calls
|
|
42
|
-
- builds system/user context into the model request
|
|
43
|
-
- streams assistant deltas and messages to the UI
|
|
44
|
-
- collects `tool_use` events, executes tools, and feeds `tool_result` messages into the next model turn
|
|
45
|
-
- tracks `previousResponseId` for Responses API tool-result continuation
|
|
46
|
-
- emits terminal reasons such as `completed`, `max_turns`, `model_error`, and abort states
|
|
47
|
-
- keeps max-output-token recovery and reactive compact continuation points explicit
|
|
48
|
-
|
|
49
|
-
`npm run smoke:core` verifies the tool-call follow-up loop with a fake model and the built-in `echo` tool.
|
|
50
|
-
|
|
51
|
-
## Tool System
|
|
52
|
-
|
|
53
|
-
`src/tools` implements the Chapter 02 tool system contract:
|
|
54
|
-
|
|
55
|
-
- tools are lifecycle objects with identity, aliases, schemas, metadata, validators, execution, result mapping, progress rendering, and optional context modifiers
|
|
56
|
-
- `ToolRegistry` keeps built-in tools as a stable prompt-cache prefix, supports aliases, filters deferred tools, and merges external tools deterministically
|
|
57
|
-
- `runToolUse()` performs schema validation, custom validation, permission decision, progress emission, abort handling, result mapping, max-result truncation, new messages, and context modifier propagation
|
|
58
|
-
- `runTools()` partitions tool calls into concurrency-safe batches and serial batches, applying context modifiers in tool-use order
|
|
59
|
-
- `StreamingToolExecutor` can start tools as tool calls arrive and can synthesize discarded results on fallback/abort
|
|
60
|
-
- `grepTool` calls the bundled ripgrep binary through `ripgrep-binary.ts`, supports smart/sensitive/insensitive case modes, glob filters, hidden-file opt-in, bounded context lines, and bounded total results
|
|
61
|
-
- `searchTool` performs web search through a pluggable `SearchProvider`; the initial backend is Exa MCP, with provider factory seams for future Bing/Tavily/custom implementations
|
|
62
|
-
- `scripts/install-ripgrep.cjs` resolves the current OS/CPU, downloads the matching official ripgrep release asset, extracts `rg`, and writes a manifest beside the binary; runtime lookup does not depend on PATH
|
|
63
|
-
|
|
64
|
-
`npm run smoke:tools` verifies aliases, schema/custom validation, unknown-tool errors, max result truncation, bundled-rg grep, pluggable web search, and concurrent batch execution.
|
|
65
|
-
|
|
66
|
-
## Context And Prompts
|
|
67
|
-
|
|
68
|
-
`src/context` implements the Chapter 03 and Chapter 05 prompt/context path:
|
|
69
|
-
|
|
70
|
-
- `prompts.ts` builds system prompt sections with `__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__`, replacement priority, proactive agent append mode, and prefix splitting for cache-aware providers
|
|
71
|
-
- `DefaultContextManager` memoizes user context (`currentDate`, project memory files) and system context (`cwd`, platform, git branch/status/recent commit)
|
|
72
|
-
- `message-pipeline.ts` prepends user context as a user message, appends system context to the system prompt, respects compact boundaries, and budgets oversized tool results
|
|
73
|
-
- `ModelDrivenCompactor` uses the configured model gateway for autocompact and reactive compact summaries
|
|
74
|
-
- deterministic snip, microcompact, and summary fallback remain available when model summarization fails or when tests need predictable output
|
|
75
|
-
- `query.ts` persists compact-boundary messages into the event stream and retries once after provider `context_length` errors
|
|
76
|
-
|
|
77
|
-
`npm run smoke:context` verifies prompt boundary splitting, context injection, tool result budgeting, deterministic compaction, model-driven autocompact, and prompt-too-long recovery.
|
|
78
|
-
|
|
79
|
-
## Session Resume
|
|
80
|
-
|
|
81
|
-
The REPL writes JSONL transcripts under `.agent/sessions` by default. After each user message, a background title subagent check is scheduled after 5s: it creates one initial title, then performs one later refinement with the conversation and previous title if the prior title agent has finished. `/sessions` opens an interactive browser (default 10 per page): use ↑/↓ to select a session, ←/→ to switch pages when more than one page exists, Enter to resume the selected session, and Esc to close.
|
|
82
|
-
|
|
83
|
-
Startup resume is available with environment variables:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
set AGENT_SESSION_RESUME=1
|
|
87
|
-
npm run dev
|
|
88
|
-
|
|
89
|
-
set AGENT_SESSION_ID=<session_id>
|
|
90
|
-
set AGENT_SESSION_RESUME=1
|
|
91
|
-
npm run dev
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Set `AGENT_SESSION_TRANSCRIPT=0` to disable transcript persistence, or `AGENT_SESSION_DIR=<absolute-or-relative-dir>` to store transcripts elsewhere. `/reset` clears the active history and records a reset marker so future resumes start after the reset.
|
|
95
|
-
|
|
96
|
-
`npm run smoke:session` verifies transcript recording, latest-session lookup, specific-session resume, tool-result output persistence, and reset markers.
|
|
97
|
-
|
|
98
|
-
## Subagents And Tasks
|
|
99
|
-
|
|
100
|
-
`src/agents` and `src/tasks` implement the Chapter 04 core path:
|
|
101
|
-
|
|
102
|
-
- `AgentTool` is a normal tool with `prompt`, `description`, `subagent_type`, `model`, `run_in_background`, `name`, `team_name`, `mode`, `isolation`, and `cwd` inputs
|
|
103
|
-
- `AgentTool` now exports prompt rules covering fresh/fork/background/parallel/prompt-quality behavior
|
|
104
|
-
- `runAgent()` creates isolated child messages/context/tool pools and reuses the same `query()` loop
|
|
105
|
-
- `AgentDefinition` supports tool allow/deny lists, model, effort, permission mode, background, max turns, memory, isolation, and custom system prompts
|
|
106
|
-
- sync agents return a completed structured result; background/fork agents register `LocalAgentTask` and return `async_launched`
|
|
107
|
-
- `TaskOutput`, `TaskList`, `TaskGet`, `TaskStop`, and `SendMessage` provide the minimum control surface for background agents
|
|
108
|
-
- completed background tasks write `.agent-tasks/<task_id>.txt`; the directory is gitignored
|
|
109
|
-
- fork children get explicit anti-recursion and scope boilerplate; teammate/team inputs are represented as named background agents for now
|
|
110
|
-
|
|
111
|
-
`npm run smoke:agents` verifies sync delegation, async launch, task output, output-file persistence, task listing, and named-agent message routing.
|
|
112
|
-
|
|
113
|
-
## Skills
|
|
114
|
-
|
|
115
|
-
`src/skills` implements the low-risk part of Chapter 05 SkillTool:
|
|
116
|
-
|
|
117
|
-
- `SkillTool` validates skill existence and model invocation eligibility
|
|
118
|
-
- inline skills inject a meta user message into the next model turn through `newMessages`
|
|
119
|
-
- inline skills can update main loop model and effort through `contextModifier`
|
|
120
|
-
- fork skills are recognized and return a structured `fork_required` result instead of silently pretending to run
|
|
121
|
-
|
|
122
|
-
`npm run smoke:skills` verifies inline injection, context modification, fork-skill rejection, and unknown-skill validation.
|
|
123
|
-
|
|
124
|
-
## Model Providers
|
|
125
|
-
|
|
126
|
-
The REPL loads environment files, reads `MODEL_*` settings into a small discriminated provider config, and constructs a provider through `provider-factory.ts`. It loads the current directory `.env` first, then overrides it with the user-level config at `%APPDATA%\\neo\\.env` on Windows or `~/.config/neo/.env` on other platforms. If the user-level config file is missing, `neo` creates a commented template there and prints a startup notice telling the user to fill `MODEL_API_KEY`. Set `NEO_ENV_FILE` to point at a custom env file; that file has the highest priority. Provider-specific switches stay inside provider-owned config (`OpenAIProviderConfig.openai.endpoint` today), while `OPENAI_*` variables remain supported as compatibility aliases.
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
mkdir "%APPDATA%\\neo"
|
|
130
|
-
notepad "%APPDATA%\\neo\\.env"
|
|
131
|
-
|
|
132
|
-
# In the env file:
|
|
133
|
-
MODEL_PROVIDER=openai
|
|
134
|
-
MODEL_API_KEY=your-api-key
|
|
135
|
-
MODEL_BASE_URL=https://api.openai.com
|
|
136
|
-
MODEL_ID=gpt-5.5
|
|
137
|
-
MODEL_REASONING_EFFORT=high
|
|
138
|
-
MODEL_ENDPOINT=auto
|
|
139
|
-
|
|
140
|
-
npm run smoke:openai -- "Say pong"
|
|
141
|
-
npm run dev
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
The OpenAI adapter is split into a small provider facade plus mappers:
|
|
145
|
-
|
|
146
|
-
- `openai-adapter.ts`: endpoint selection, auth, transport, retry, and Responses-to-Chat fallback
|
|
147
|
-
- `openai-responses-mapper.ts`: `/v1/responses` request and event mapping
|
|
148
|
-
- `openai-chat-mapper.ts`: OpenAI-compatible `/v1/chat/completions` fallback mapping
|
|
149
|
-
- `openai-mappers.ts`: shared tool/message/usage helpers
|
|
150
|
-
|
|
151
|
-
`MODEL_ENDPOINT=auto` tries `/v1/responses` first and falls back to `/v1/chat/completions` for OpenAI-compatible gateways that do not expose Responses API.
|
|
1
|
+
# Agent Scaffold Source
|
|
2
|
+
|
|
3
|
+
This directory is a TypeScript implementation scaffold for the parent README. Chapters 01, 02, 03, 04, 05-core, and 07 now have runnable paths; later chapters still expose stable module boundaries and placeholders.
|
|
4
|
+
|
|
5
|
+
## Shape
|
|
6
|
+
|
|
7
|
+
- `src/repl`: the UI layer. It owns terminal input, slash commands, system init events, and rendering streamed events.
|
|
8
|
+
- `src/core`: the multi-turn query loop, loop state, message pipeline, `QueryEngine`, and child-agent runner entry points.
|
|
9
|
+
- `src/model`: provider-neutral model gateway/config/factory, provider adapters, OpenAI Responses/Chat mappers, HTTP transport, SSE decoder, retry, and normalized errors.
|
|
10
|
+
- `src/tools`: lifecycle tool contracts, registry, schema validation, execution pipeline, batch orchestration, streaming executor, and built-in tools.
|
|
11
|
+
- `src/context`: system prompt sections, runtime user/system context, deterministic compaction helpers, and model-driven compaction.
|
|
12
|
+
- `src/session`: JSONL session transcripts, tool-result persistence, latest/specific session resume, and session listing.
|
|
13
|
+
- `src/agents`: agent definitions, `AgentTool`, prompt rules, fork constraints, local task lifecycle, and agent smoke coverage.
|
|
14
|
+
- `src/tasks`: background task store, task-control tools, named-agent message routing, and persisted task output files.
|
|
15
|
+
- `src/skills`: inline workflow-as-tool injection and fork-skill boundary.
|
|
16
|
+
- `src/safety`: optional permission, sandbox, and audit ports.
|
|
17
|
+
- `src/app`: app-state ports used by tools and runtime code.
|
|
18
|
+
- `vendor/ripgrep`: per-platform bundled `rg` binaries installed by `npm run vendor:rg` or optional `postinstall`.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
npm run vendor:rg
|
|
25
|
+
npm run typecheck
|
|
26
|
+
npm run build
|
|
27
|
+
npm run smoke:core
|
|
28
|
+
npm run smoke:tools
|
|
29
|
+
npm run smoke:context
|
|
30
|
+
npm run smoke:agents
|
|
31
|
+
npm run smoke:skills
|
|
32
|
+
npm run smoke:openai -- "Say pong"
|
|
33
|
+
npm run dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Core Loop
|
|
37
|
+
|
|
38
|
+
`src/core/query.ts` implements the Chapter 01 main path as a streaming state machine:
|
|
39
|
+
|
|
40
|
+
- prepares each turn from a single `QueryState`
|
|
41
|
+
- applies compact-boundary filtering and tool-result budgeting before model calls
|
|
42
|
+
- builds system/user context into the model request
|
|
43
|
+
- streams assistant deltas and messages to the UI
|
|
44
|
+
- collects `tool_use` events, executes tools, and feeds `tool_result` messages into the next model turn
|
|
45
|
+
- tracks `previousResponseId` for Responses API tool-result continuation
|
|
46
|
+
- emits terminal reasons such as `completed`, `max_turns`, `model_error`, and abort states
|
|
47
|
+
- keeps max-output-token recovery and reactive compact continuation points explicit
|
|
48
|
+
|
|
49
|
+
`npm run smoke:core` verifies the tool-call follow-up loop with a fake model and the built-in `echo` tool.
|
|
50
|
+
|
|
51
|
+
## Tool System
|
|
52
|
+
|
|
53
|
+
`src/tools` implements the Chapter 02 tool system contract:
|
|
54
|
+
|
|
55
|
+
- tools are lifecycle objects with identity, aliases, schemas, metadata, validators, execution, result mapping, progress rendering, and optional context modifiers
|
|
56
|
+
- `ToolRegistry` keeps built-in tools as a stable prompt-cache prefix, supports aliases, filters deferred tools, and merges external tools deterministically
|
|
57
|
+
- `runToolUse()` performs schema validation, custom validation, permission decision, progress emission, abort handling, result mapping, max-result truncation, new messages, and context modifier propagation
|
|
58
|
+
- `runTools()` partitions tool calls into concurrency-safe batches and serial batches, applying context modifiers in tool-use order
|
|
59
|
+
- `StreamingToolExecutor` can start tools as tool calls arrive and can synthesize discarded results on fallback/abort
|
|
60
|
+
- `grepTool` calls the bundled ripgrep binary through `ripgrep-binary.ts`, supports smart/sensitive/insensitive case modes, glob filters, hidden-file opt-in, bounded context lines, and bounded total results
|
|
61
|
+
- `searchTool` performs web search through a pluggable `SearchProvider`; the initial backend is Exa MCP, with provider factory seams for future Bing/Tavily/custom implementations
|
|
62
|
+
- `scripts/install-ripgrep.cjs` resolves the current OS/CPU, downloads the matching official ripgrep release asset, extracts `rg`, and writes a manifest beside the binary; runtime lookup does not depend on PATH
|
|
63
|
+
|
|
64
|
+
`npm run smoke:tools` verifies aliases, schema/custom validation, unknown-tool errors, max result truncation, bundled-rg grep, pluggable web search, and concurrent batch execution.
|
|
65
|
+
|
|
66
|
+
## Context And Prompts
|
|
67
|
+
|
|
68
|
+
`src/context` implements the Chapter 03 and Chapter 05 prompt/context path:
|
|
69
|
+
|
|
70
|
+
- `prompts.ts` builds system prompt sections with `__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__`, replacement priority, proactive agent append mode, and prefix splitting for cache-aware providers
|
|
71
|
+
- `DefaultContextManager` memoizes user context (`currentDate`, project memory files) and system context (`cwd`, platform, git branch/status/recent commit)
|
|
72
|
+
- `message-pipeline.ts` prepends user context as a user message, appends system context to the system prompt, respects compact boundaries, and budgets oversized tool results
|
|
73
|
+
- `ModelDrivenCompactor` uses the configured model gateway for autocompact and reactive compact summaries
|
|
74
|
+
- deterministic snip, microcompact, and summary fallback remain available when model summarization fails or when tests need predictable output
|
|
75
|
+
- `query.ts` persists compact-boundary messages into the event stream and retries once after provider `context_length` errors
|
|
76
|
+
|
|
77
|
+
`npm run smoke:context` verifies prompt boundary splitting, context injection, tool result budgeting, deterministic compaction, model-driven autocompact, and prompt-too-long recovery.
|
|
78
|
+
|
|
79
|
+
## Session Resume
|
|
80
|
+
|
|
81
|
+
The REPL writes JSONL transcripts under `.agent/sessions` by default. After each user message, a background title subagent check is scheduled after 5s: it creates one initial title, then performs one later refinement with the conversation and previous title if the prior title agent has finished. `/sessions` opens an interactive browser (default 10 per page): use ↑/↓ to select a session, ←/→ to switch pages when more than one page exists, Enter to resume the selected session, and Esc to close.
|
|
82
|
+
|
|
83
|
+
Startup resume is available with environment variables:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
set AGENT_SESSION_RESUME=1
|
|
87
|
+
npm run dev
|
|
88
|
+
|
|
89
|
+
set AGENT_SESSION_ID=<session_id>
|
|
90
|
+
set AGENT_SESSION_RESUME=1
|
|
91
|
+
npm run dev
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Set `AGENT_SESSION_TRANSCRIPT=0` to disable transcript persistence, or `AGENT_SESSION_DIR=<absolute-or-relative-dir>` to store transcripts elsewhere. `/reset` clears the active history and records a reset marker so future resumes start after the reset.
|
|
95
|
+
|
|
96
|
+
`npm run smoke:session` verifies transcript recording, latest-session lookup, specific-session resume, tool-result output persistence, and reset markers.
|
|
97
|
+
|
|
98
|
+
## Subagents And Tasks
|
|
99
|
+
|
|
100
|
+
`src/agents` and `src/tasks` implement the Chapter 04 core path:
|
|
101
|
+
|
|
102
|
+
- `AgentTool` is a normal tool with `prompt`, `description`, `subagent_type`, `model`, `run_in_background`, `name`, `team_name`, `mode`, `isolation`, and `cwd` inputs
|
|
103
|
+
- `AgentTool` now exports prompt rules covering fresh/fork/background/parallel/prompt-quality behavior
|
|
104
|
+
- `runAgent()` creates isolated child messages/context/tool pools and reuses the same `query()` loop
|
|
105
|
+
- `AgentDefinition` supports tool allow/deny lists, model, effort, permission mode, background, max turns, memory, isolation, and custom system prompts
|
|
106
|
+
- sync agents return a completed structured result; background/fork agents register `LocalAgentTask` and return `async_launched`
|
|
107
|
+
- `TaskOutput`, `TaskList`, `TaskGet`, `TaskStop`, and `SendMessage` provide the minimum control surface for background agents
|
|
108
|
+
- completed background tasks write `.agent-tasks/<task_id>.txt`; the directory is gitignored
|
|
109
|
+
- fork children get explicit anti-recursion and scope boilerplate; teammate/team inputs are represented as named background agents for now
|
|
110
|
+
|
|
111
|
+
`npm run smoke:agents` verifies sync delegation, async launch, task output, output-file persistence, task listing, and named-agent message routing.
|
|
112
|
+
|
|
113
|
+
## Skills
|
|
114
|
+
|
|
115
|
+
`src/skills` implements the low-risk part of Chapter 05 SkillTool:
|
|
116
|
+
|
|
117
|
+
- `SkillTool` validates skill existence and model invocation eligibility
|
|
118
|
+
- inline skills inject a meta user message into the next model turn through `newMessages`
|
|
119
|
+
- inline skills can update main loop model and effort through `contextModifier`
|
|
120
|
+
- fork skills are recognized and return a structured `fork_required` result instead of silently pretending to run
|
|
121
|
+
|
|
122
|
+
`npm run smoke:skills` verifies inline injection, context modification, fork-skill rejection, and unknown-skill validation.
|
|
123
|
+
|
|
124
|
+
## Model Providers
|
|
125
|
+
|
|
126
|
+
The REPL loads environment files, reads `MODEL_*` settings into a small discriminated provider config, and constructs a provider through `provider-factory.ts`. It loads the current directory `.env` first, then overrides it with the user-level config at `%APPDATA%\\neo\\.env` on Windows or `~/.config/neo/.env` on other platforms. If the user-level config file is missing, `neo` creates a commented template there and prints a startup notice telling the user to fill `MODEL_API_KEY`. Set `NEO_ENV_FILE` to point at a custom env file; that file has the highest priority. Provider-specific switches stay inside provider-owned config (`OpenAIProviderConfig.openai.endpoint` today), while `OPENAI_*` variables remain supported as compatibility aliases.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
mkdir "%APPDATA%\\neo"
|
|
130
|
+
notepad "%APPDATA%\\neo\\.env"
|
|
131
|
+
|
|
132
|
+
# In the env file:
|
|
133
|
+
MODEL_PROVIDER=openai
|
|
134
|
+
MODEL_API_KEY=your-api-key
|
|
135
|
+
MODEL_BASE_URL=https://api.openai.com
|
|
136
|
+
MODEL_ID=gpt-5.5
|
|
137
|
+
MODEL_REASONING_EFFORT=high
|
|
138
|
+
MODEL_ENDPOINT=auto
|
|
139
|
+
|
|
140
|
+
npm run smoke:openai -- "Say pong"
|
|
141
|
+
npm run dev
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The OpenAI adapter is split into a small provider facade plus mappers:
|
|
145
|
+
|
|
146
|
+
- `openai-adapter.ts`: endpoint selection, auth, transport, retry, and Responses-to-Chat fallback
|
|
147
|
+
- `openai-responses-mapper.ts`: `/v1/responses` request and event mapping
|
|
148
|
+
- `openai-chat-mapper.ts`: OpenAI-compatible `/v1/chat/completions` fallback mapping
|
|
149
|
+
- `openai-mappers.ts`: shared tool/message/usage helpers
|
|
150
|
+
|
|
151
|
+
`MODEL_ENDPOINT=auto` tries `/v1/responses` first and falls back to `/v1/chat/completions` for OpenAI-compatible gateways that do not expose Responses API.
|
|
@@ -15,20 +15,25 @@ export interface ContextBudgetOptions {
|
|
|
15
15
|
compactModel?: string;
|
|
16
16
|
compactMaxOutputTokens?: number;
|
|
17
17
|
}
|
|
18
|
+
export type CompactionReason = "none" | "snip" | "microcompact" | "autocompact" | "reactive_compact" | "manualcompact" | "purecompact";
|
|
18
19
|
export interface CompactionResult {
|
|
19
20
|
messages: Message[];
|
|
20
21
|
summary?: string;
|
|
21
22
|
changed: boolean;
|
|
22
|
-
reason?:
|
|
23
|
+
reason?: CompactionReason;
|
|
23
24
|
tokensFreed?: number;
|
|
24
25
|
}
|
|
25
26
|
export interface Compactor {
|
|
26
27
|
compact(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
28
|
+
manualCompact?(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
29
|
+
pureCompact?(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
27
30
|
reactiveCompact?(messages: readonly Message[], error: Error, options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
28
31
|
}
|
|
29
32
|
export declare const CLEARED_TOOL_RESULT_CONTENT = "[Old tool result content cleared]";
|
|
30
33
|
export declare class DeterministicCompactor implements Compactor {
|
|
31
34
|
compact(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
35
|
+
manualCompact(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
36
|
+
pureCompact(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
32
37
|
reactiveCompact(messages: readonly Message[], error: Error, options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
33
38
|
}
|
|
34
39
|
export declare class ModelDrivenCompactor implements Compactor {
|
|
@@ -36,12 +41,16 @@ export declare class ModelDrivenCompactor implements Compactor {
|
|
|
36
41
|
private readonly fallback;
|
|
37
42
|
constructor(modelGateway: ModelGateway);
|
|
38
43
|
compact(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
44
|
+
manualCompact(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
45
|
+
pureCompact(messages: readonly Message[], options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
39
46
|
reactiveCompact(messages: readonly Message[], error: Error, options?: ContextBudgetOptions): Promise<CompactionResult>;
|
|
40
47
|
private modelAutoCompactIfNeeded;
|
|
41
48
|
private summarizeWithModel;
|
|
42
49
|
}
|
|
43
50
|
export declare class NoopCompactor implements Compactor {
|
|
44
51
|
compact(messages: readonly Message[]): Promise<CompactionResult>;
|
|
52
|
+
manualCompact(messages: readonly Message[]): Promise<CompactionResult>;
|
|
53
|
+
pureCompact(messages: readonly Message[]): Promise<CompactionResult>;
|
|
45
54
|
}
|
|
46
55
|
export declare function estimateMessagesChars(messages: readonly Message[]): number;
|
|
47
56
|
export declare function snipCompactIfNeeded(messages: readonly Message[], options?: ContextBudgetOptions): CompactionResult;
|
|
@@ -13,6 +13,12 @@ export class DeterministicCompactor {
|
|
|
13
13
|
return micro;
|
|
14
14
|
return { messages: [...messages], changed: false, reason: "none" };
|
|
15
15
|
}
|
|
16
|
+
async manualCompact(messages, options = {}) {
|
|
17
|
+
return manualCompactWithSummary(messages, options);
|
|
18
|
+
}
|
|
19
|
+
async pureCompact(messages, options = {}) {
|
|
20
|
+
return pureCompactWithSanitizedSummary(messages, options);
|
|
21
|
+
}
|
|
16
22
|
async reactiveCompact(messages, error, options = {}) {
|
|
17
23
|
const micro = microCompactIfNeeded(messages, options);
|
|
18
24
|
const reactive = reactiveCompactWithSummary(micro.messages, `Reactive compact after model context error: ${error.message}`, options);
|
|
@@ -37,6 +43,23 @@ export class ModelDrivenCompactor {
|
|
|
37
43
|
return micro;
|
|
38
44
|
return { messages: [...messages], changed: false, reason: "none" };
|
|
39
45
|
}
|
|
46
|
+
async manualCompact(messages, options = {}) {
|
|
47
|
+
const keepRecentMessages = options.keepRecentMessages ?? 8;
|
|
48
|
+
const recent = messages.slice(-keepRecentMessages);
|
|
49
|
+
const older = messages.slice(0, Math.max(0, messages.length - keepRecentMessages));
|
|
50
|
+
if (older.length === 0)
|
|
51
|
+
return { messages: [...messages], changed: false, reason: "none" };
|
|
52
|
+
try {
|
|
53
|
+
const summary = await this.summarizeWithModel(older, MANUAL_COMPACT_INSTRUCTIONS, options);
|
|
54
|
+
return buildCompactionResult(messages, recent, summary, "manualcompact", true);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return this.fallback.manualCompact?.(messages, options) ?? manualCompactWithSummary(messages, options);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async pureCompact(messages, options = {}) {
|
|
61
|
+
return pureCompactWithSanitizedSummary(messages, options);
|
|
62
|
+
}
|
|
40
63
|
async reactiveCompact(messages, error, options = {}) {
|
|
41
64
|
const micro = microCompactIfNeeded(messages, options);
|
|
42
65
|
const keepRecentMessages = options.keepRecentMessages ?? 8;
|
|
@@ -104,6 +127,12 @@ export class NoopCompactor {
|
|
|
104
127
|
async compact(messages) {
|
|
105
128
|
return { messages: [...messages], changed: false, reason: "none" };
|
|
106
129
|
}
|
|
130
|
+
async manualCompact(messages) {
|
|
131
|
+
return { messages: [...messages], changed: false, reason: "none" };
|
|
132
|
+
}
|
|
133
|
+
async pureCompact(messages) {
|
|
134
|
+
return { messages: [...messages], changed: false, reason: "none" };
|
|
135
|
+
}
|
|
107
136
|
}
|
|
108
137
|
export function estimateMessagesChars(messages) {
|
|
109
138
|
return messages.reduce((total, message) => total + serializeMessage(message).length, 0);
|
|
@@ -192,6 +221,15 @@ function shouldCompactForBudget(messages, options, fallbackMaxChars, triggerRati
|
|
|
192
221
|
}
|
|
193
222
|
return estimateMessagesChars(messages) > fallbackMaxChars;
|
|
194
223
|
}
|
|
224
|
+
function manualCompactWithSummary(messages, options) {
|
|
225
|
+
const keepRecentMessages = options.keepRecentMessages ?? 8;
|
|
226
|
+
const recent = messages.slice(-keepRecentMessages);
|
|
227
|
+
const older = messages.slice(0, Math.max(0, messages.length - keepRecentMessages));
|
|
228
|
+
if (older.length === 0)
|
|
229
|
+
return { messages: [...messages], changed: false, reason: "none" };
|
|
230
|
+
const summary = buildHistorySummary(older, options.summaryMaxChars ?? 6000);
|
|
231
|
+
return buildCompactionResult(messages, recent, summary || "No older messages were available to summarize.", "manualcompact", false);
|
|
232
|
+
}
|
|
195
233
|
function reactiveCompactWithSummary(messages, heading, options) {
|
|
196
234
|
const keepRecentMessages = options.keepRecentMessages ?? 8;
|
|
197
235
|
const recent = messages.slice(-keepRecentMessages);
|
|
@@ -199,8 +237,25 @@ function reactiveCompactWithSummary(messages, heading, options) {
|
|
|
199
237
|
const summary = buildHistorySummary(older, options.summaryMaxChars ?? 6000);
|
|
200
238
|
return buildCompactionResult(messages, recent, `${heading}\n\n${summary || "No older messages were available to summarize."}`, "reactive_compact", false);
|
|
201
239
|
}
|
|
240
|
+
function pureCompactWithSanitizedSummary(messages, options) {
|
|
241
|
+
if (messages.length === 0)
|
|
242
|
+
return { messages: [], changed: false, reason: "none" };
|
|
243
|
+
const summary = buildPureSummary(messages, options.summaryMaxChars ?? 4000);
|
|
244
|
+
const boundary = createCompactionBoundaryMessage(`Pure compacted conversation after a transport/WAF risk block.\n\n${summary}`, "purecompact", false);
|
|
245
|
+
return {
|
|
246
|
+
messages: [boundary],
|
|
247
|
+
summary,
|
|
248
|
+
changed: true,
|
|
249
|
+
reason: "purecompact",
|
|
250
|
+
tokensFreed: Math.max(0, estimateMessagesChars(messages) - estimateMessagesChars([boundary])),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
202
253
|
function buildCompactionResult(originalMessages, recentMessages, summary, reason, modelDriven) {
|
|
203
|
-
const label = reason === "autocompact"
|
|
254
|
+
const label = reason === "autocompact"
|
|
255
|
+
? "Auto compacted earlier conversation."
|
|
256
|
+
: reason === "manualcompact"
|
|
257
|
+
? "Manually compacted earlier conversation."
|
|
258
|
+
: "Reactive compacted earlier conversation.";
|
|
204
259
|
const boundary = createCompactionBoundaryMessage(`${label}\n\n${summary}`, reason, modelDriven);
|
|
205
260
|
const compacted = [boundary, ...recentMessages];
|
|
206
261
|
return {
|
|
@@ -255,6 +310,104 @@ function buildHistorySummary(messages, maxChars) {
|
|
|
255
310
|
const joined = lines.join("\n");
|
|
256
311
|
return joined.length > maxChars ? `${joined.slice(0, maxChars)}\n- ...summary truncated...` : joined;
|
|
257
312
|
}
|
|
313
|
+
function buildPureSummary(messages, maxChars) {
|
|
314
|
+
const userLines = [];
|
|
315
|
+
const assistantLines = [];
|
|
316
|
+
const toolLines = [];
|
|
317
|
+
const stateLines = [];
|
|
318
|
+
for (const message of messages) {
|
|
319
|
+
const text = sanitizeForPureState(serializeMessageForPure(message));
|
|
320
|
+
if (!text)
|
|
321
|
+
continue;
|
|
322
|
+
const line = `- ${text}`;
|
|
323
|
+
if (message.metadata?.compactBoundary === true)
|
|
324
|
+
stateLines.push(line);
|
|
325
|
+
else if (message.role === "user")
|
|
326
|
+
userLines.push(line);
|
|
327
|
+
else if (message.role === "assistant")
|
|
328
|
+
assistantLines.push(line);
|
|
329
|
+
else if (message.role === "tool_result")
|
|
330
|
+
toolLines.push(line);
|
|
331
|
+
else if (message.role !== "system" && message.role !== "progress" && message.role !== "tombstone")
|
|
332
|
+
stateLines.push(`- ${message.role}: ${text}`);
|
|
333
|
+
}
|
|
334
|
+
const sections = [
|
|
335
|
+
"Purpose: sanitized continuation state produced by /pure after a risk/WAF block; raw commands, logs, code snippets, and bulky tool output were intentionally omitted.",
|
|
336
|
+
formatPureSection("Recent user goals/constraints", lastItems(userLines, 10)),
|
|
337
|
+
formatPureSection("Recent assistant progress/decisions", lastItems(assistantLines, 8)),
|
|
338
|
+
formatPureSection("Tool activity summary", lastItems(toolLines, 8)),
|
|
339
|
+
formatPureSection("Prior compact/state facts", lastItems(stateLines, 8)),
|
|
340
|
+
"Pending work: continue from the latest user request using the sanitized facts above; ask for clarification only if a required detail was removed by sanitization.",
|
|
341
|
+
].filter(Boolean).join("\n");
|
|
342
|
+
return sections.length > maxChars ? `${sections.slice(0, maxChars)}\n- ...pure summary truncated...` : sections;
|
|
343
|
+
}
|
|
344
|
+
function formatPureSection(title, lines) {
|
|
345
|
+
return lines.length > 0 ? `${title}:\n${dedupeLines(lines).join("\n")}` : `${title}: none retained.`;
|
|
346
|
+
}
|
|
347
|
+
function lastItems(items, limit) {
|
|
348
|
+
return items.slice(Math.max(0, items.length - limit));
|
|
349
|
+
}
|
|
350
|
+
function dedupeLines(lines) {
|
|
351
|
+
const seen = new Set();
|
|
352
|
+
const deduped = [];
|
|
353
|
+
for (const line of lines) {
|
|
354
|
+
if (seen.has(line))
|
|
355
|
+
continue;
|
|
356
|
+
seen.add(line);
|
|
357
|
+
deduped.push(line);
|
|
358
|
+
}
|
|
359
|
+
return deduped;
|
|
360
|
+
}
|
|
361
|
+
function serializeMessageForPure(message) {
|
|
362
|
+
return message.blocks
|
|
363
|
+
.map((block) => {
|
|
364
|
+
if (block.type === "text")
|
|
365
|
+
return block.text;
|
|
366
|
+
if (block.type === "image")
|
|
367
|
+
return block.label ?? `[image ${block.mimeType}]`;
|
|
368
|
+
if (block.type === "thinking")
|
|
369
|
+
return "[assistant thinking omitted]";
|
|
370
|
+
if (block.type === "tool_use")
|
|
371
|
+
return `tool_use ${block.name}: ${sanitizeToolPayload(block.input)}`;
|
|
372
|
+
if (block.type === "tool_result")
|
|
373
|
+
return `tool_result ${block.name}: ${block.ok ? "ok" : "error"}`;
|
|
374
|
+
return "";
|
|
375
|
+
})
|
|
376
|
+
.filter(Boolean)
|
|
377
|
+
.join("\n");
|
|
378
|
+
}
|
|
379
|
+
function sanitizeToolPayload(input) {
|
|
380
|
+
const serialized = typeof input === "string" ? input : JSON.stringify(input);
|
|
381
|
+
if (!serialized)
|
|
382
|
+
return "no input";
|
|
383
|
+
return sanitizeForPureState(serialized).slice(0, 180) || "details omitted";
|
|
384
|
+
}
|
|
385
|
+
function sanitizeForPureState(text) {
|
|
386
|
+
const normalized = text.replace(/```[\s\S]*?```/g, "[code block omitted]");
|
|
387
|
+
const lines = normalized
|
|
388
|
+
.split(/\r?\n/)
|
|
389
|
+
.map((line) => sanitizePureLine(line))
|
|
390
|
+
.filter(Boolean);
|
|
391
|
+
return dedupeLines(lines).join("; ").replace(/\s+/g, " ").trim().slice(0, 700);
|
|
392
|
+
}
|
|
393
|
+
function sanitizePureLine(line) {
|
|
394
|
+
let safe = line.trim();
|
|
395
|
+
if (!safe)
|
|
396
|
+
return "";
|
|
397
|
+
if (/\b(python\s+-c|node\s+-e|bash\s+-c|sh\s+-c|powershell|cmd\.exe|set-location|copy-item|invoke-webrequest|curl|read_text|write_text)\b/i.test(safe)) {
|
|
398
|
+
return "[omitted raw command/log detail]";
|
|
399
|
+
}
|
|
400
|
+
safe = safe.replace(/[A-Za-z]:[\\/][^\s"'`<>]+/g, (match) => summarizePathForPureState(match));
|
|
401
|
+
safe = safe.replace(/[\\]+/g, "/");
|
|
402
|
+
safe = safe.replace(/[{}<>`]/g, " ");
|
|
403
|
+
safe = safe.replace(/\s+/g, " ").trim();
|
|
404
|
+
return safe.length > 240 ? `${safe.slice(0, 240)}...` : safe;
|
|
405
|
+
}
|
|
406
|
+
function summarizePathForPureState(value) {
|
|
407
|
+
const parts = value.split(/[\\/]+/).filter(Boolean);
|
|
408
|
+
const tail = parts.slice(Math.max(0, parts.length - 3)).join("/");
|
|
409
|
+
return tail ? `[path:${tail}]` : "[path]";
|
|
410
|
+
}
|
|
258
411
|
function serializeTranscriptForSummary(messages, maxChars) {
|
|
259
412
|
const lines = messages.map((message) => {
|
|
260
413
|
const metadata = [
|
|
@@ -324,6 +477,13 @@ function extractText(message) {
|
|
|
324
477
|
function buildReactiveCompactInstruction(error) {
|
|
325
478
|
return `${AUTO_COMPACT_INSTRUCTIONS}\n\nThe previous model request failed because the prompt was too long. Preserve enough task state to continue after this error. Error: ${error.message}`;
|
|
326
479
|
}
|
|
480
|
+
const MANUAL_COMPACT_INSTRUCTIONS = [
|
|
481
|
+
"Summarize the earlier agent conversation for continuation after a user-requested context compaction.",
|
|
482
|
+
"Preserve: user goals and constraints, decisions made, files or commands mentioned, completed work, pending work, task ids, important tool results, and any errors or blockers.",
|
|
483
|
+
"Drop: repetitive logs, transient progress chatter, and irrelevant wording.",
|
|
484
|
+
"Return concise plain text labels, not Markdown headings. Use labels like Goal:, Constraints:, Work completed:, Important facts:, Pending work:, Open risks:.",
|
|
485
|
+
"Do not include final-answer prose; this summary is an internal continuation state only.",
|
|
486
|
+
].join("\n");
|
|
327
487
|
const AUTO_COMPACT_INSTRUCTIONS = [
|
|
328
488
|
"Summarize the earlier agent conversation for continuation after context compaction.",
|
|
329
489
|
"Preserve: user goals and constraints, decisions made, files or commands mentioned, completed work, pending work, task ids, important tool results, and any errors or blockers.",
|