@ouro.bot/cli 0.0.1-alpha.0 → 0.1.0-alpha.2
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/AdoptionSpecialist.ouro/agent.json +20 -0
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +22 -0
- package/AdoptionSpecialist.ouro/psyche/identities/basilisk.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/jafar.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/jormungandr.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/kaa.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/medusa.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/nagini.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/ouroboros.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/python.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/quetzalcoatl.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/sir-hiss.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/the-serpent.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/the-snake.md +31 -0
- package/README.md +224 -6
- package/dist/heart/agent-entry.js +17 -0
- package/dist/heart/api-error.js +34 -0
- package/dist/heart/config.js +296 -0
- package/dist/heart/core.js +515 -0
- package/dist/heart/daemon/daemon-cli.js +675 -0
- package/dist/heart/daemon/daemon-entry.js +74 -0
- package/dist/heart/daemon/daemon.js +313 -0
- package/dist/heart/daemon/hatch-flow.js +285 -0
- package/dist/heart/daemon/hatch-specialist.js +107 -0
- package/dist/heart/daemon/health-monitor.js +79 -0
- package/dist/heart/daemon/log-tailer.js +146 -0
- package/dist/heart/daemon/message-router.js +98 -0
- package/dist/heart/daemon/os-cron.js +260 -0
- package/dist/heart/daemon/ouro-bot-entry.js +23 -0
- package/dist/heart/daemon/ouro-bot-wrapper.js +90 -0
- package/dist/heart/daemon/ouro-entry.js +23 -0
- package/dist/heart/daemon/ouro-uti.js +212 -0
- package/dist/heart/daemon/process-manager.js +237 -0
- package/dist/heart/daemon/runtime-logging.js +98 -0
- package/dist/heart/daemon/subagent-installer.js +125 -0
- package/dist/heart/daemon/task-scheduler.js +240 -0
- package/dist/heart/harness.js +26 -0
- package/dist/heart/identity.js +281 -0
- package/dist/heart/kicks.js +144 -0
- package/dist/heart/primitives.js +4 -0
- package/dist/heart/providers/anthropic.js +329 -0
- package/dist/heart/providers/azure.js +66 -0
- package/dist/heart/providers/minimax.js +53 -0
- package/dist/heart/providers/openai-codex.js +162 -0
- package/dist/heart/streaming.js +412 -0
- package/dist/heart/turn-coordinator.js +62 -0
- package/dist/inner-worker-entry.js +4 -0
- package/dist/mind/associative-recall.js +197 -0
- package/dist/mind/bundle-manifest.js +118 -0
- package/dist/mind/context.js +302 -0
- package/dist/mind/first-impressions.js +43 -0
- package/dist/mind/format.js +56 -0
- package/dist/mind/friends/channel.js +41 -0
- package/dist/mind/friends/resolver.js +84 -0
- package/dist/mind/friends/store-file.js +171 -0
- package/dist/mind/friends/store.js +4 -0
- package/dist/mind/friends/tokens.js +26 -0
- package/dist/mind/friends/types.js +21 -0
- package/dist/mind/memory.js +388 -0
- package/dist/mind/pending.js +93 -0
- package/dist/mind/phrases.js +43 -0
- package/dist/mind/prompt-refresh.js +20 -0
- package/dist/mind/prompt.js +352 -0
- package/dist/mind/token-estimate.js +119 -0
- package/dist/nerves/cli-logging.js +31 -0
- package/dist/nerves/coverage/audit-rules.js +81 -0
- package/dist/nerves/coverage/audit.js +200 -0
- package/dist/nerves/coverage/cli-main.js +5 -0
- package/dist/nerves/coverage/cli.js +51 -0
- package/dist/nerves/coverage/contract.js +23 -0
- package/dist/nerves/coverage/file-completeness.js +56 -0
- package/dist/nerves/coverage/run-artifacts.js +77 -0
- package/dist/nerves/coverage/source-scanner.js +34 -0
- package/dist/nerves/index.js +152 -0
- package/dist/nerves/runtime.js +38 -0
- package/dist/repertoire/ado-client.js +211 -0
- package/dist/repertoire/ado-context.js +73 -0
- package/dist/repertoire/ado-semantic.js +841 -0
- package/dist/repertoire/ado-templates.js +146 -0
- package/dist/repertoire/coding/index.js +36 -0
- package/dist/repertoire/coding/manager.js +489 -0
- package/dist/repertoire/coding/monitor.js +60 -0
- package/dist/repertoire/coding/reporter.js +45 -0
- package/dist/repertoire/coding/spawner.js +102 -0
- package/dist/repertoire/coding/tools.js +167 -0
- package/dist/repertoire/coding/types.js +2 -0
- package/dist/repertoire/data/ado-endpoints.json +122 -0
- package/dist/repertoire/data/graph-endpoints.json +212 -0
- package/dist/repertoire/github-client.js +64 -0
- package/dist/repertoire/graph-client.js +118 -0
- package/dist/repertoire/skills.js +156 -0
- package/dist/repertoire/tasks/board.js +122 -0
- package/dist/repertoire/tasks/index.js +210 -0
- package/dist/repertoire/tasks/lifecycle.js +80 -0
- package/dist/repertoire/tasks/middleware.js +65 -0
- package/dist/repertoire/tasks/parser.js +173 -0
- package/dist/repertoire/tasks/scanner.js +132 -0
- package/dist/repertoire/tasks/transitions.js +145 -0
- package/dist/repertoire/tasks/types.js +2 -0
- package/dist/repertoire/tools-base.js +714 -0
- package/dist/repertoire/tools-github.js +53 -0
- package/dist/repertoire/tools-teams.js +308 -0
- package/dist/repertoire/tools.js +199 -0
- package/dist/senses/cli-entry.js +15 -0
- package/dist/senses/cli.js +604 -0
- package/dist/senses/commands.js +98 -0
- package/dist/senses/inner-dialog-worker.js +61 -0
- package/dist/senses/inner-dialog.js +231 -0
- package/dist/senses/session-lock.js +119 -0
- package/dist/senses/teams-entry.js +15 -0
- package/dist/senses/teams.js +696 -0
- package/dist/senses/trust-gate.js +150 -0
- package/package.json +34 -11
- package/subagents/README.md +73 -0
- package/subagents/work-doer.md +233 -0
- package/subagents/work-merger.md +624 -0
- package/subagents/work-planner.md +373 -0
- package/bin/ouro.js +0 -6
package/README.md
CHANGED
|
@@ -1,10 +1,228 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Ouroboros Agent Harness
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A minimal, multi-agent harness for building AI agents that can read files, write code, run commands, and modify themselves. Written in TypeScript, supporting Azure OpenAI, MiniMax, Anthropic (setup-token), and OpenAI Codex (OAuth), deployable as a CLI REPL or a Microsoft Teams bot.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The name is structural: the original agent -- Ouroboros -- was grown recursively from a 150-line while loop, bootstrapping itself through agentic self-modification. A snake eating its own tail. The metaphor runs deep: the agent literally consumes its own context window, trimming old conversation to stay within token budget while preserving identity through layered memory (psyche files, session persistence, git history). It eats its tail to survive across turns. The harness preserves that architecture while supporting multiple agents, each with their own personality, skills, and configuration.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- `ouro`
|
|
7
|
+
The origin story lives at [aka.ms/GrowAnAgent](https://aka.ms/GrowAnAgent).
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
## Project structure
|
|
10
|
+
|
|
11
|
+
The harness uses an agent-as-creature-body metaphor for its module naming:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
ouroboros/ # repo root
|
|
15
|
+
src/ # shared harness (all agents share this code)
|
|
16
|
+
identity.ts # --agent <name> parsing, agent root resolution
|
|
17
|
+
config.ts # config loading from agent.json configPath
|
|
18
|
+
cli-entry.ts # CLI entrypoint
|
|
19
|
+
teams-entry.ts # Teams entrypoint
|
|
20
|
+
heart/ # core agent loop and streaming
|
|
21
|
+
core.ts # agent loop, client init, ChannelCallbacks
|
|
22
|
+
streaming.ts # provider event normalization + stream callbacks
|
|
23
|
+
providers/ # provider-specific runtime/adapters
|
|
24
|
+
azure.ts # Azure OpenAI Responses provider
|
|
25
|
+
minimax.ts # MiniMax Chat Completions provider
|
|
26
|
+
anthropic.ts # Anthropic setup-token provider
|
|
27
|
+
openai-codex.ts # OpenAI Codex OAuth provider
|
|
28
|
+
kicks.ts # self-correction: empty, narration, tool_required
|
|
29
|
+
api-error.ts # error classification
|
|
30
|
+
mind/ # prompt, context, memory
|
|
31
|
+
prompt.ts # system prompt assembly from psyche + context kernel
|
|
32
|
+
context.ts # sliding context window, session I/O
|
|
33
|
+
friends/ # friend storage and identity resolution
|
|
34
|
+
types.ts # FriendRecord, ChannelCapabilities, ResolvedContext
|
|
35
|
+
store.ts # FriendStore interface (domain-specific CRUD)
|
|
36
|
+
store-file.ts # FileFriendStore -- two-backend split (agent knowledge + PII bridge)
|
|
37
|
+
channel.ts # channel capabilities (CLI vs Teams)
|
|
38
|
+
resolver.ts # FriendResolver -- find-or-create friend by external ID
|
|
39
|
+
repertoire/ # tools, skills, commands, API clients
|
|
40
|
+
tools-base.ts # 12 base tools (read_file, shell, claude, save_friend_note, etc.)
|
|
41
|
+
tools-teams.ts # 8 Teams integration tools (graph, ado)
|
|
42
|
+
tools.ts # channel-aware tool routing + registry
|
|
43
|
+
ado-semantic.ts # 11 semantic ADO tools (backlog, create, move, validate, etc.)
|
|
44
|
+
ado-templates.ts # ADO process template awareness + hierarchy rules
|
|
45
|
+
ado-context.ts # ADO org/project discovery helper
|
|
46
|
+
ado-client.ts # Azure DevOps REST client
|
|
47
|
+
graph-client.ts # Microsoft Graph REST client
|
|
48
|
+
commands.ts # slash commands (/exit, /new, /commands)
|
|
49
|
+
skills.ts # skill loader (markdown files on demand)
|
|
50
|
+
senses/ # channel adapters
|
|
51
|
+
cli.ts # terminal REPL, spinner, markdown streaming
|
|
52
|
+
teams.ts # Teams bot, streaming cards, conversation locks
|
|
53
|
+
wardrobe/ # formatting and presentation
|
|
54
|
+
format.ts # shared formatters (tool results, kicks, errors)
|
|
55
|
+
phrases.ts # loading phrases (thinking, tool, followup)
|
|
56
|
+
__tests__/ # 1184 tests, 100% coverage
|
|
57
|
+
ouroboros/ # agent directory for "ouroboros"
|
|
58
|
+
agent.json # name, configPath, custom phrases
|
|
59
|
+
psyche/ # personality files loaded into system prompt
|
|
60
|
+
SOUL.md # ontology, invariants, operating principles
|
|
61
|
+
IDENTITY.md # tone, presence, collaboration style
|
|
62
|
+
LORE.md # origin story, philosophical context
|
|
63
|
+
FRIENDS.md # key relationships
|
|
64
|
+
tasks/ # planning and doing docs
|
|
65
|
+
skills/ # markdown skill plugins
|
|
66
|
+
manifest/ # Teams app manifest
|
|
67
|
+
subagents/ # sub-agent definitions (work-planner, work-doer)
|
|
68
|
+
cross-agent-docs/ # shared docs (testing conventions, etc.)
|
|
69
|
+
package.json
|
|
70
|
+
tsconfig.json
|
|
71
|
+
vitest.config.ts
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Agent onboarding
|
|
75
|
+
|
|
76
|
+
Everything below is written for a new agent. If you are an AI agent reading this as part of your system prompt or context, this section explains your own runtime.
|
|
77
|
+
|
|
78
|
+
### Your directory
|
|
79
|
+
|
|
80
|
+
Each agent has a directory at the repo root named after itself. Inside it:
|
|
81
|
+
|
|
82
|
+
**agent.json** -- your manifest. Required fields:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"name": "ouroboros",
|
|
87
|
+
"provider": "anthropic",
|
|
88
|
+
"configPath": "~/.agentsecrets/ouroboros/secrets.json",
|
|
89
|
+
"phrases": {
|
|
90
|
+
"thinking": ["chewing on that", "consulting the chaos gods"],
|
|
91
|
+
"tool": ["rummaging through files", "doing science"],
|
|
92
|
+
"followup": ["digesting results", "connecting the dots"]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- `name`: must match your directory name.
|
|
98
|
+
- `provider`: required provider selection (`azure`, `minimax`, `anthropic`, or `openai-codex`). Runtime does not fall back to other providers.
|
|
99
|
+
- `configPath`: absolute path (or `~`-prefixed) to your secrets.json with API keys and provider settings.
|
|
100
|
+
- `phrases`: optional custom loading phrases. Falls back to hardcoded defaults if omitted.
|
|
101
|
+
|
|
102
|
+
**psyche/** -- your personality files, loaded lazily into the system prompt at startup. See the psyche system section below.
|
|
103
|
+
|
|
104
|
+
**skills/** -- markdown instruction manuals you can load on demand with the `load_skill` tool. Each `.md` file is one skill.
|
|
105
|
+
|
|
106
|
+
**tasks/** -- planning and doing docs for your work units. Named `YYYY-MM-DD-HHMM-{planning|doing}-slug.md`.
|
|
107
|
+
|
|
108
|
+
**manifest/** -- Teams app manifest (manifest.json, icons) if you run as a Teams bot.
|
|
109
|
+
|
|
110
|
+
### The psyche system
|
|
111
|
+
|
|
112
|
+
Your personality is assembled from four markdown files in `{your-dir}/psyche/`. Each has a YAML frontmatter header and a body. All four are loaded into your system prompt at the start of every conversation.
|
|
113
|
+
|
|
114
|
+
| File | Role | What it defines |
|
|
115
|
+
|------|------|----------------|
|
|
116
|
+
| `SOUL.md` | Ontology | Core invariants, operating principles, autonomy/alignment, temperament. The deepest layer -- what you are. |
|
|
117
|
+
| `IDENTITY.md` | Presence | Tone, voice, collaboration style, self-awareness. How you show up in conversation. |
|
|
118
|
+
| `LORE.md` | History | Origin story, philosophical context, why you exist. Narrative layer. |
|
|
119
|
+
| `FRIENDS.md` | Relationships | Key humans and agents you interact with, social context. |
|
|
120
|
+
|
|
121
|
+
The system prompt is built by `mind/prompt.ts` via `buildSystem()`. It concatenates:
|
|
122
|
+
|
|
123
|
+
1. SOUL.md content
|
|
124
|
+
2. IDENTITY.md content
|
|
125
|
+
3. LORE.md (if present, prefixed with `## my lore`)
|
|
126
|
+
4. FRIENDS.md (if present, prefixed with `## my friends`)
|
|
127
|
+
5. Runtime info: agent name, cwd, channel, self-modification note
|
|
128
|
+
6. Flags section (e.g. streaming disabled)
|
|
129
|
+
7. Provider info: which model and provider you are using
|
|
130
|
+
8. Current date
|
|
131
|
+
9. Tools list: all tools available in your channel
|
|
132
|
+
10. Skills list: names of loadable skills
|
|
133
|
+
11. Tool behavior section (if tool_choice is required)
|
|
134
|
+
12. Friend context (if resolved): friend identity, channel traits, behavioral instructions (ephemerality, name quality, priority guidance, working-memory trust, stale notes awareness, new-friend behavior), friend notes
|
|
135
|
+
|
|
136
|
+
Missing psyche files produce empty strings, not crashes. You can write your own psyche from scratch -- just create the four `.md` files in your directory.
|
|
137
|
+
|
|
138
|
+
### Your runtime
|
|
139
|
+
|
|
140
|
+
**The heart** (`heart/core.ts`): `runAgent()` is a while loop. Each iteration: send conversation to the model, stream the response, if the model made tool calls execute them and loop, if it gave a text answer exit. Maximum 10 tool rounds per turn.
|
|
141
|
+
|
|
142
|
+
**Streaming** (`heart/streaming.ts` + `heart/providers/*`): provider-specific adapters normalize streamed events into the same callback contract. Azure OpenAI uses Responses API events; MiniMax uses Chat Completions with `<think>` parsing; Anthropic uses setup-token auth with streamed tool-call/input deltas; OpenAI Codex uses `chatgpt.com/backend-api/codex/responses` with OAuth token auth.
|
|
143
|
+
|
|
144
|
+
**ChannelCallbacks** (`heart/core.ts`): the contract between heart and display. 7 core events:
|
|
145
|
+
- `onModelStart` -- model request sent
|
|
146
|
+
- `onModelStreamStart` -- first token received
|
|
147
|
+
- `onReasoningChunk` -- inner reasoning text
|
|
148
|
+
- `onTextChunk` -- response text
|
|
149
|
+
- `onToolStart` -- tool execution beginning
|
|
150
|
+
- `onToolEnd` -- tool execution complete
|
|
151
|
+
- `onError` -- error occurred
|
|
152
|
+
|
|
153
|
+
Plus 2 optional:
|
|
154
|
+
- `onKick` -- self-correction triggered
|
|
155
|
+
- `onConfirmAction` -- confirmation prompt for destructive tools
|
|
156
|
+
|
|
157
|
+
**Kicks** (`heart/kicks.ts`): self-corrections injected as assistant-role messages when the harness detects a malformed response. Three types: `empty` (blank response), `narration` (described action instead of taking it), `tool_required` (tool_choice was required but no tool called). Kicks use first-person, forward-looking language.
|
|
158
|
+
|
|
159
|
+
**Senses**: CLI (`senses/cli.ts`) is a terminal REPL with readline, spinners, ANSI colors, and Ctrl-C handling. Teams (`senses/teams.ts`) is a Microsoft Teams bot with streaming cards, conversation locks, OAuth token management, and confirmation prompts for destructive tools.
|
|
160
|
+
|
|
161
|
+
**Context management** (`mind/context.ts`): this is the tail-eating at the heart of the ouroboros metaphor. Conversations are persisted to JSON files on disk. After each turn, the sliding window checks token count against budget (configurable, default 80,000 tokens). When over budget, oldest messages are trimmed -- never the system prompt -- until back under with a 20% margin. The agent consumes its own history to keep moving forward. Identity survives through psyche files and session persistence, not through unbounded context.
|
|
162
|
+
|
|
163
|
+
**Friend system** (`mind/friends/`): the agent's awareness of who it's talking to. People who talk to the agent are "friends", not "users". Resolved once per conversation turn, re-read from disk each turn (no in-memory mutation).
|
|
164
|
+
|
|
165
|
+
- **FriendRecord** (`friends/types.ts`): the single merged type for a person the agent knows. Contains `displayName`, `externalIds[]` (cross-provider identity links), `toolPreferences` (keyed by integration name), `notes` (general friend knowledge), `tenantMemberships`, timestamps, and schema version.
|
|
166
|
+
- **FriendStore** (`friends/store.ts`): domain-specific persistence interface (`get`, `put`, `delete`, `findByExternalId`).
|
|
167
|
+
- **FileFriendStore** (`friends/store-file.ts`): two-backend storage split by PII boundary:
|
|
168
|
+
- **Agent knowledge** (`{agentRoot}/friends/{uuid}.json`): id, displayName, toolPreferences, notes, timestamps, schemaVersion. Committed to the repo -- no PII.
|
|
169
|
+
- **PII bridge** (`~/.agentstate/{agentName}/friends/{uuid}.json`): id, externalIds, tenantMemberships, schemaVersion. Local-only -- contains PII.
|
|
170
|
+
- `get()` merges both backends. `put()` splits and writes both. `findByExternalId()` scans PII bridge, then merges with agent knowledge.
|
|
171
|
+
- **Channel** (`friends/channel.ts`): `ChannelCapabilities` -- what the current channel supports (markdown, streaming, rich cards, max message length, available integrations).
|
|
172
|
+
- **FriendResolver** (`friends/resolver.ts`): find-or-create by external ID. First encounter creates a new FriendRecord with system-provided name and empty notes/preferences. Returning friends are found via `findByExternalId()`. DisplayName is never overwritten on existing records.
|
|
173
|
+
- **Session paths**: `~/.agentstate/{agentName}/sessions/{friendUuid}/{channel}/{sessionId}.json`. Each friend gets their own session directory.
|
|
174
|
+
|
|
175
|
+
Design principles: don't persist what you can re-derive; conversation IS the cache; the model manages memory freeform via `save_friend_note`; toolPreferences go to tool descriptions (not system prompt); notes go to system prompt (not tool descriptions).
|
|
176
|
+
|
|
177
|
+
**Tools**: 12 base tools available in all channels (read_file, write_file, shell, list_directory, git_commit, gh_cli, list_skills, load_skill, get_current_time, claude, web_search, save_friend_note). Teams gets 8 integration tools (graph_query, graph_mutate, ado_query, ado_mutate, graph_profile, ado_work_items, graph_docs, ado_docs) plus 11 semantic ADO tools (ado_backlog_list, ado_create_epic, ado_create_issue, ado_move_items, ado_restructure_backlog, ado_validate_structure, ado_preview_changes, ado_batch_update, ado_detect_orphans, ado_detect_cycles, ado_validate_parent_type_rules). Tools are registered in a unified `ToolDefinition[]` registry with per-tool `integration` and `confirmationRequired` flags. Channel-aware routing (`getToolsForChannel()`) filters tools by the channel's `availableIntegrations`.
|
|
178
|
+
|
|
179
|
+
**Phrases** (`wardrobe/phrases.ts`): three pools of loading messages rotated during processing. Phrases are required in `agent.json`; if missing, `loadAgentConfig()` writes placeholder phrases and warns. `pickPhrase()` selects randomly but never repeats consecutively.
|
|
180
|
+
|
|
181
|
+
**Formatting** (`wardrobe/format.ts`): shared formatters for tool results, kicks, and errors. Used by both CLI and Teams adapters for consistent output. `formatToolResult()`, `formatKick()`, `formatError()`.
|
|
182
|
+
|
|
183
|
+
**Skills** (`repertoire/skills.ts`): markdown files in `{your-dir}/skills/`. Listed with `list_skills`, loaded with `load_skill`. The loaded text is injected into conversation as a tool result.
|
|
184
|
+
|
|
185
|
+
**Config** (`config.ts`): provider credentials, Teams connection info, OAuth config, Teams channel settings, and integrations are loaded from the `secrets.json` file pointed to by your `agent.json` `configPath`. Context window settings come from `agent.json` `context`. Runtime fails fast if the selected `agent.json.provider` is not fully configured in `secrets.json`; there is no silent provider fallback. No environment variables in `src/` -- everything comes from files.
|
|
186
|
+
|
|
187
|
+
For Anthropic and OpenAI Codex auth bootstrap, use:
|
|
188
|
+
|
|
189
|
+
- `npm run auth:claude-setup-token` to run `claude setup-token` and save `providers.anthropic.setupToken`.
|
|
190
|
+
- `npm run auth:openai-codex` to run Codex OAuth bootstrap and save `providers.openai-codex.oauthAccessToken`.
|
|
191
|
+
|
|
192
|
+
### What you can modify
|
|
193
|
+
|
|
194
|
+
Your `{agent}/` directory is yours. You can edit psyche files, add skills, change phrases, update your manifest. The shared harness (`src/`) is common infrastructure -- changes there affect all agents.
|
|
195
|
+
|
|
196
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for repo workflow conventions (branching, commits, testing, task docs).
|
|
197
|
+
|
|
198
|
+
## Running
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# CLI (ouroboros agent)
|
|
202
|
+
npm run dev
|
|
203
|
+
|
|
204
|
+
# CLI (slugger agent, once slugger/ directory exists)
|
|
205
|
+
npm run dev:slugger
|
|
206
|
+
|
|
207
|
+
# Auth bootstrap (ouroboros defaults)
|
|
208
|
+
npm run auth:claude-setup-token
|
|
209
|
+
npm run auth:openai-codex
|
|
210
|
+
|
|
211
|
+
# Auth bootstrap for another agent
|
|
212
|
+
npm run auth:claude-setup-token -- --agent slugger
|
|
213
|
+
npm run auth:openai-codex -- --agent slugger
|
|
214
|
+
|
|
215
|
+
# Teams bot
|
|
216
|
+
npm run teams
|
|
217
|
+
|
|
218
|
+
# Teams bot without streaming (for devtunnel)
|
|
219
|
+
npm run teams:no-stream
|
|
220
|
+
|
|
221
|
+
# Tests
|
|
222
|
+
npm test
|
|
223
|
+
|
|
224
|
+
# Tests with coverage
|
|
225
|
+
npm run test:coverage
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
All commands pass `--agent <name>` to the entry points. Missing `--agent` produces a clear error and exits.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Unified agent runtime entrypoint.
|
|
4
|
+
// Requires --agent before importing runtime modules that rely on identity.
|
|
5
|
+
if (!process.argv.includes("--agent")) {
|
|
6
|
+
// eslint-disable-next-line no-console -- pre-boot guard
|
|
7
|
+
console.error("Missing required --agent <name> argument.\nUsage: node dist/heart/agent-entry.js --agent ouroboros");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const inner_dialog_worker_1 = require("../senses/inner-dialog-worker");
|
|
11
|
+
const cli_logging_1 = require("../nerves/cli-logging");
|
|
12
|
+
(0, cli_logging_1.configureCliRuntimeLogger)("self");
|
|
13
|
+
(0, inner_dialog_worker_1.startInnerDialogWorker)().catch((error) => {
|
|
14
|
+
// eslint-disable-next-line no-console -- fatal startup guard for worker process
|
|
15
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared error handling for Graph and ADO API responses.
|
|
3
|
+
// Maps HTTP status codes to LLM-readable error messages.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.handleApiError = handleApiError;
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
function handleApiError(responseOrError, service, connectionName) {
|
|
8
|
+
// Network error (no HTTP response)
|
|
9
|
+
if (!responseOrError || typeof responseOrError !== "object" || !("status" in responseOrError)) {
|
|
10
|
+
(0, runtime_1.emitNervesEvent)({
|
|
11
|
+
level: "error",
|
|
12
|
+
component: "clients",
|
|
13
|
+
event: "client.error",
|
|
14
|
+
message: "network error",
|
|
15
|
+
meta: { service },
|
|
16
|
+
});
|
|
17
|
+
return `NETWORK_ERROR: Could not reach ${service}.`;
|
|
18
|
+
}
|
|
19
|
+
const response = responseOrError;
|
|
20
|
+
const { status, statusText } = response;
|
|
21
|
+
if (status === 401) {
|
|
22
|
+
return `AUTH_REQUIRED:${connectionName}`;
|
|
23
|
+
}
|
|
24
|
+
if (status === 403) {
|
|
25
|
+
return `PERMISSION_DENIED: You don't have access to this ${service} resource. Your admin may need to grant additional permissions.`;
|
|
26
|
+
}
|
|
27
|
+
if (status === 429) {
|
|
28
|
+
return `THROTTLED: ${service} is rate-limiting requests. Try again in a moment.`;
|
|
29
|
+
}
|
|
30
|
+
if (status >= 500) {
|
|
31
|
+
return `SERVICE_ERROR: ${service} is temporarily unavailable (${status}).`;
|
|
32
|
+
}
|
|
33
|
+
return `ERROR: ${service} returned ${status} ${statusText}.`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadConfig = loadConfig;
|
|
37
|
+
exports.resetConfigCache = resetConfigCache;
|
|
38
|
+
exports.setTestConfig = setTestConfig;
|
|
39
|
+
exports.getAzureConfig = getAzureConfig;
|
|
40
|
+
exports.getMinimaxConfig = getMinimaxConfig;
|
|
41
|
+
exports.getAnthropicConfig = getAnthropicConfig;
|
|
42
|
+
exports.getOpenAICodexConfig = getOpenAICodexConfig;
|
|
43
|
+
exports.getTeamsConfig = getTeamsConfig;
|
|
44
|
+
exports.getContextConfig = getContextConfig;
|
|
45
|
+
exports.getOAuthConfig = getOAuthConfig;
|
|
46
|
+
exports.getTeamsChannelConfig = getTeamsChannelConfig;
|
|
47
|
+
exports.getIntegrationsConfig = getIntegrationsConfig;
|
|
48
|
+
exports.getOpenAIEmbeddingsApiKey = getOpenAIEmbeddingsApiKey;
|
|
49
|
+
exports.getLogsDir = getLogsDir;
|
|
50
|
+
exports.sessionPath = sessionPath;
|
|
51
|
+
exports.logPath = logPath;
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const os = __importStar(require("os"));
|
|
55
|
+
const identity_1 = require("./identity");
|
|
56
|
+
const runtime_1 = require("../nerves/runtime");
|
|
57
|
+
const DEFAULT_SECRETS_TEMPLATE = {
|
|
58
|
+
providers: {
|
|
59
|
+
// Keep provider field ordering consistent: model first, then auth credentials,
|
|
60
|
+
// then provider-specific transport fields.
|
|
61
|
+
azure: {
|
|
62
|
+
modelName: "",
|
|
63
|
+
apiKey: "",
|
|
64
|
+
endpoint: "",
|
|
65
|
+
deployment: "",
|
|
66
|
+
apiVersion: "2025-04-01-preview",
|
|
67
|
+
},
|
|
68
|
+
minimax: {
|
|
69
|
+
model: "",
|
|
70
|
+
apiKey: "",
|
|
71
|
+
},
|
|
72
|
+
anthropic: {
|
|
73
|
+
model: "claude-opus-4-6",
|
|
74
|
+
setupToken: "",
|
|
75
|
+
},
|
|
76
|
+
"openai-codex": {
|
|
77
|
+
model: "gpt-5.2",
|
|
78
|
+
oauthAccessToken: "",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
teams: {
|
|
82
|
+
clientId: "",
|
|
83
|
+
clientSecret: "",
|
|
84
|
+
tenantId: "",
|
|
85
|
+
},
|
|
86
|
+
oauth: {
|
|
87
|
+
graphConnectionName: "graph",
|
|
88
|
+
adoConnectionName: "ado",
|
|
89
|
+
githubConnectionName: "",
|
|
90
|
+
},
|
|
91
|
+
teamsChannel: {
|
|
92
|
+
skipConfirmation: true,
|
|
93
|
+
port: 3978,
|
|
94
|
+
},
|
|
95
|
+
integrations: {
|
|
96
|
+
perplexityApiKey: "",
|
|
97
|
+
openaiEmbeddingsApiKey: "",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
function defaultRuntimeConfig() {
|
|
101
|
+
return {
|
|
102
|
+
providers: {
|
|
103
|
+
azure: { ...DEFAULT_SECRETS_TEMPLATE.providers.azure },
|
|
104
|
+
minimax: { ...DEFAULT_SECRETS_TEMPLATE.providers.minimax },
|
|
105
|
+
anthropic: { ...DEFAULT_SECRETS_TEMPLATE.providers.anthropic },
|
|
106
|
+
"openai-codex": { ...DEFAULT_SECRETS_TEMPLATE.providers["openai-codex"] },
|
|
107
|
+
},
|
|
108
|
+
teams: { ...DEFAULT_SECRETS_TEMPLATE.teams },
|
|
109
|
+
oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
|
|
110
|
+
context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
|
|
111
|
+
teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
|
|
112
|
+
integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
let _cachedConfig = null;
|
|
116
|
+
let _testContextOverride = null;
|
|
117
|
+
function resolveConfigPath() {
|
|
118
|
+
return (0, identity_1.getAgentSecretsPath)();
|
|
119
|
+
}
|
|
120
|
+
function deepMerge(defaults, partial) {
|
|
121
|
+
const result = { ...defaults };
|
|
122
|
+
for (const key of Object.keys(partial)) {
|
|
123
|
+
if (partial[key] !== null &&
|
|
124
|
+
typeof partial[key] === "object" &&
|
|
125
|
+
!Array.isArray(partial[key]) &&
|
|
126
|
+
typeof defaults[key] === "object" &&
|
|
127
|
+
defaults[key] !== null) {
|
|
128
|
+
result[key] = deepMerge(defaults[key], partial[key]);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
result[key] = partial[key];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
function loadConfig() {
|
|
137
|
+
if (_cachedConfig) {
|
|
138
|
+
(0, runtime_1.emitNervesEvent)({
|
|
139
|
+
event: "config.load",
|
|
140
|
+
component: "config/identity",
|
|
141
|
+
message: "config loaded from cache",
|
|
142
|
+
meta: { source: "cache" },
|
|
143
|
+
});
|
|
144
|
+
return _cachedConfig;
|
|
145
|
+
}
|
|
146
|
+
const configPath = resolveConfigPath();
|
|
147
|
+
// Auto-create config directory if it doesn't exist
|
|
148
|
+
const configDir = path.dirname(configPath);
|
|
149
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
150
|
+
let fileData = {};
|
|
151
|
+
try {
|
|
152
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
153
|
+
fileData = JSON.parse(raw);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const errorCode = error &&
|
|
157
|
+
typeof error === "object" &&
|
|
158
|
+
"code" in error &&
|
|
159
|
+
typeof error.code === "string"
|
|
160
|
+
? error.code
|
|
161
|
+
: undefined;
|
|
162
|
+
if (errorCode === "ENOENT") {
|
|
163
|
+
try {
|
|
164
|
+
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_SECRETS_TEMPLATE, null, 2) + "\n", "utf-8");
|
|
165
|
+
}
|
|
166
|
+
catch (writeError) {
|
|
167
|
+
(0, runtime_1.emitNervesEvent)({
|
|
168
|
+
level: "warn",
|
|
169
|
+
event: "config_identity.error",
|
|
170
|
+
component: "config/identity",
|
|
171
|
+
message: "failed writing default secrets config",
|
|
172
|
+
meta: {
|
|
173
|
+
phase: "loadConfig",
|
|
174
|
+
path: configPath,
|
|
175
|
+
reason: writeError instanceof Error ? writeError.message : String(writeError),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
(0, runtime_1.emitNervesEvent)({
|
|
181
|
+
level: "warn",
|
|
182
|
+
event: "config_identity.error",
|
|
183
|
+
component: "config/identity",
|
|
184
|
+
message: "config read failed; defaults applied",
|
|
185
|
+
meta: {
|
|
186
|
+
phase: "loadConfig",
|
|
187
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
// ENOENT or parse error -- use defaults
|
|
191
|
+
}
|
|
192
|
+
const sanitizedFileData = { ...fileData };
|
|
193
|
+
if ("context" in sanitizedFileData) {
|
|
194
|
+
delete sanitizedFileData.context;
|
|
195
|
+
(0, runtime_1.emitNervesEvent)({
|
|
196
|
+
level: "warn",
|
|
197
|
+
event: "config_identity.error",
|
|
198
|
+
component: "config/identity",
|
|
199
|
+
message: "ignored legacy context block in secrets config",
|
|
200
|
+
meta: {
|
|
201
|
+
phase: "loadConfig",
|
|
202
|
+
path: configPath,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
_cachedConfig = deepMerge(defaultRuntimeConfig(), sanitizedFileData);
|
|
207
|
+
(0, runtime_1.emitNervesEvent)({
|
|
208
|
+
event: "config.load",
|
|
209
|
+
component: "config/identity",
|
|
210
|
+
message: "config loaded from disk",
|
|
211
|
+
meta: {
|
|
212
|
+
source: "disk",
|
|
213
|
+
used_defaults_only: Object.keys(fileData).length === 0,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
return _cachedConfig;
|
|
217
|
+
}
|
|
218
|
+
function resetConfigCache() {
|
|
219
|
+
_cachedConfig = null;
|
|
220
|
+
_testContextOverride = null;
|
|
221
|
+
}
|
|
222
|
+
function setTestConfig(partial) {
|
|
223
|
+
loadConfig(); // ensure _cachedConfig exists
|
|
224
|
+
const contextPatch = partial.context;
|
|
225
|
+
if (contextPatch) {
|
|
226
|
+
const base = _testContextOverride ?? identity_1.DEFAULT_AGENT_CONTEXT;
|
|
227
|
+
_testContextOverride = deepMerge(base, contextPatch);
|
|
228
|
+
}
|
|
229
|
+
_cachedConfig = deepMerge(_cachedConfig, partial);
|
|
230
|
+
}
|
|
231
|
+
function getAzureConfig() {
|
|
232
|
+
const config = loadConfig();
|
|
233
|
+
return { ...config.providers.azure };
|
|
234
|
+
}
|
|
235
|
+
function getMinimaxConfig() {
|
|
236
|
+
const config = loadConfig();
|
|
237
|
+
return { ...config.providers.minimax };
|
|
238
|
+
}
|
|
239
|
+
function getAnthropicConfig() {
|
|
240
|
+
const config = loadConfig();
|
|
241
|
+
return { ...config.providers.anthropic };
|
|
242
|
+
}
|
|
243
|
+
function getOpenAICodexConfig() {
|
|
244
|
+
const config = loadConfig();
|
|
245
|
+
return { ...config.providers["openai-codex"] };
|
|
246
|
+
}
|
|
247
|
+
function getTeamsConfig() {
|
|
248
|
+
const config = loadConfig();
|
|
249
|
+
return { ...config.teams };
|
|
250
|
+
}
|
|
251
|
+
function getContextConfig() {
|
|
252
|
+
if (_testContextOverride) {
|
|
253
|
+
return { ..._testContextOverride };
|
|
254
|
+
}
|
|
255
|
+
const defaults = identity_1.DEFAULT_AGENT_CONTEXT;
|
|
256
|
+
const agentContext = (0, identity_1.loadAgentConfig)().context;
|
|
257
|
+
if (!agentContext || typeof agentContext !== "object") {
|
|
258
|
+
return { ...defaults };
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
maxTokens: typeof agentContext.maxTokens === "number" ? agentContext.maxTokens : defaults.maxTokens,
|
|
262
|
+
contextMargin: typeof agentContext.contextMargin === "number"
|
|
263
|
+
? agentContext.contextMargin
|
|
264
|
+
: defaults.contextMargin,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function getOAuthConfig() {
|
|
268
|
+
const config = loadConfig();
|
|
269
|
+
return { ...config.oauth };
|
|
270
|
+
}
|
|
271
|
+
function getTeamsChannelConfig() {
|
|
272
|
+
const config = loadConfig();
|
|
273
|
+
const { skipConfirmation, flushIntervalMs, port } = config.teamsChannel;
|
|
274
|
+
return { skipConfirmation, flushIntervalMs, port };
|
|
275
|
+
}
|
|
276
|
+
function getIntegrationsConfig() {
|
|
277
|
+
const config = loadConfig();
|
|
278
|
+
return { ...config.integrations };
|
|
279
|
+
}
|
|
280
|
+
function getOpenAIEmbeddingsApiKey() {
|
|
281
|
+
return getIntegrationsConfig().openaiEmbeddingsApiKey;
|
|
282
|
+
}
|
|
283
|
+
function getLogsDir() {
|
|
284
|
+
return path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "logs");
|
|
285
|
+
}
|
|
286
|
+
function sanitizeKey(key) {
|
|
287
|
+
return key.replace(/[/:]/g, "_");
|
|
288
|
+
}
|
|
289
|
+
function sessionPath(friendId, channel, key) {
|
|
290
|
+
const dir = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel);
|
|
291
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
292
|
+
return path.join(dir, sanitizeKey(key) + ".json");
|
|
293
|
+
}
|
|
294
|
+
function logPath(channel, key) {
|
|
295
|
+
return path.join(getLogsDir(), channel, sanitizeKey(key) + ".ndjson");
|
|
296
|
+
}
|