@agentic-kit/pi-host 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +129 -0
- package/esm/events.d.ts +47 -0
- package/esm/events.js +161 -0
- package/esm/gate.d.ts +35 -0
- package/esm/gate.js +88 -0
- package/esm/host.d.ts +18 -0
- package/esm/host.js +26 -0
- package/esm/index.d.ts +16 -0
- package/esm/index.js +12 -0
- package/esm/model.d.ts +39 -0
- package/esm/model.js +211 -0
- package/esm/persona.d.ts +85 -0
- package/esm/persona.js +122 -0
- package/esm/project-context.d.ts +22 -0
- package/esm/project-context.js +50 -0
- package/esm/tools.d.ts +23 -0
- package/esm/tools.js +32 -0
- package/esm/workspace-tools.d.ts +22 -0
- package/esm/workspace-tools.js +199 -0
- package/events.d.ts +47 -0
- package/events.js +168 -0
- package/gate.d.ts +35 -0
- package/gate.js +92 -0
- package/host.d.ts +18 -0
- package/host.js +29 -0
- package/index.d.ts +16 -0
- package/index.js +42 -0
- package/model.d.ts +39 -0
- package/model.js +216 -0
- package/package.json +49 -0
- package/persona.d.ts +85 -0
- package/persona.js +131 -0
- package/project-context.d.ts +22 -0
- package/project-context.js +59 -0
- package/tools.d.ts +23 -0
- package/tools.js +36 -0
- package/workspace-tools.d.ts +22 -0
- package/workspace-tools.js +207 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
|
|
4
|
+
Copyright (c) 2025 Constructive <developers@constructive.io>
|
|
5
|
+
Copyright (c) 2020-present, Interweb, Inc.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @agentic-kit/pi-host
|
|
2
|
+
|
|
3
|
+
The host side of a headless agent run. `@agentic-kit` already owns the loop
|
|
4
|
+
(`@agentic-kit/agent`), the tools (`@agentic-kit/pi`) and the gating policy
|
|
5
|
+
(`@agentic-kit/harness`); what a workload has to supply is the three small
|
|
6
|
+
host-shaped things those packages ask for, and nothing here re-implements any of
|
|
7
|
+
them.
|
|
8
|
+
|
|
9
|
+
| Existing contract | What this package supplies |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `GateHost` (`@agentic-kit/harness`) | `createThreadGateHost` — approval through the conversation thread |
|
|
12
|
+
| `ConfirmGate` (`createConfirmGate`) | `createGatedToolset` — wraps `AgentTool.execute` with the harness gate |
|
|
13
|
+
| `PiToolsHost` (`@agentic-kit/pi/host`) | `createPiToolsHost` — built from values, not from a desktop session |
|
|
14
|
+
| `AgentEvent` (`@agentic-kit/agent`) | `TranscriptWriter` — events → `agent_message` / `agent_task` |
|
|
15
|
+
|
|
16
|
+
## Approval is a message, so `hasUI` is true
|
|
17
|
+
|
|
18
|
+
`GateHost.hasUI` does not mean "a window exists": it means a human decision is
|
|
19
|
+
obtainable. A Job can obtain one — it writes the pending call into the thread at
|
|
20
|
+
`approval-requested` and waits for the human to echo it back — so the runner
|
|
21
|
+
reports `true`. Reporting `false` would tell the harness no decision is possible
|
|
22
|
+
and every tool in `MUTATING_DB_TOOLS` would be refused before it ran.
|
|
23
|
+
|
|
24
|
+
The gate is the harness's. `createGatedToolset` calls `gate.onToolCall(...)` and,
|
|
25
|
+
when the gate blocks, returns the gate's own reason to the model as an ordinary
|
|
26
|
+
tool result — a declined call is information the agent can act on, not a crash.
|
|
27
|
+
pi's fs/edit/bash/git tools are not in `MUTATING_DB_TOOLS` and are never gated:
|
|
28
|
+
the whole point of the coding lane is that editing the clone is free.
|
|
29
|
+
|
|
30
|
+
## Credentials do not go in the clone
|
|
31
|
+
|
|
32
|
+
`@agentic-kit/pi` resolves tenant context with `resolveProjectContext(cwd)`,
|
|
33
|
+
which reads `<cwd>/.env` for `ACCESS_TOKEN` and `DATABASE_ID`. In this lane `cwd`
|
|
34
|
+
is a cloned git repository that the agent is about to `git add -A` and push, so
|
|
35
|
+
writing platform credentials there is a credential-exfiltration bug waiting for
|
|
36
|
+
a careless commit. The coding lane therefore runs with the coding tool set and no
|
|
37
|
+
project context in the work tree. A lane that genuinely needs control-plane tools
|
|
38
|
+
calls `materializeProjectContext({ dir, workTree, ... })`, which writes a `0600`
|
|
39
|
+
`.env` **outside** the work tree and throws
|
|
40
|
+
`ProjectContextInsideWorkTreeError` if asked to do otherwise.
|
|
41
|
+
|
|
42
|
+
## Persona
|
|
43
|
+
|
|
44
|
+
`loadPersona` / `loadPersonaSkills` / `selectPersona` turn an `agent_persona` row
|
|
45
|
+
and the `agent_resource` rows it names into the model, system prompt, temperature
|
|
46
|
+
and tool subset for the run. A persona that asks for a tool the lane does not
|
|
47
|
+
carry throws (`UnknownPersonaToolError`) rather than running with a quietly
|
|
48
|
+
smaller toolbox, and a named resource that is missing or inactive throws rather
|
|
49
|
+
than dropping a skill the operator expected to be in force.
|
|
50
|
+
|
|
51
|
+
## Tests
|
|
52
|
+
|
|
53
|
+
`pnpm test` — the gate against a fake thread (approve / decline / cancel /
|
|
54
|
+
timeout), the writer against a fake agent run, persona selection, the gated
|
|
55
|
+
toolset, and the project-context guard.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Education and Tutorials
|
|
60
|
+
|
|
61
|
+
1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
|
|
62
|
+
Get started with modular databases in minutes. Install prerequisites and deploy your first module.
|
|
63
|
+
|
|
64
|
+
2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
|
|
65
|
+
Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
|
|
66
|
+
|
|
67
|
+
3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
|
|
68
|
+
Master the workflow for adding, organizing, and managing database changes with pgpm.
|
|
69
|
+
|
|
70
|
+
4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
|
|
71
|
+
Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
|
|
72
|
+
|
|
73
|
+
5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
|
|
74
|
+
Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
|
|
75
|
+
|
|
76
|
+
6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
|
|
77
|
+
Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
|
|
78
|
+
|
|
79
|
+
7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
|
|
80
|
+
Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
81
|
+
|
|
82
|
+
## Related Constructive Tooling
|
|
83
|
+
|
|
84
|
+
### 📦 Package Management
|
|
85
|
+
|
|
86
|
+
* [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
|
|
87
|
+
|
|
88
|
+
### 🧪 Testing
|
|
89
|
+
|
|
90
|
+
* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
|
|
91
|
+
* [pglite-test](https://github.com/constructive-io/constructive/tree/main/postgres/pglite-test): **🪶 Drop-in pgsql-test replacement backed by PGlite** — in-process Postgres, no server required, instance-per-suite isolation.
|
|
92
|
+
* [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
|
|
93
|
+
* [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
|
|
94
|
+
* [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
|
|
95
|
+
* [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
|
|
96
|
+
|
|
97
|
+
### 🧠 Parsing & AST
|
|
98
|
+
|
|
99
|
+
* [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
|
|
100
|
+
* [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
|
|
101
|
+
* [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
|
|
102
|
+
* [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
|
|
103
|
+
* [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
|
|
104
|
+
* [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
|
|
105
|
+
|
|
106
|
+
### 📚 Documentation & Skills
|
|
107
|
+
|
|
108
|
+
* [constructive-skills](https://github.com/constructive-io/constructive-skills): **📖 Platform documentation and AI agent skills** — feature catalog, blueprint reference, SDK guides (i18n, billing, limits, events, uploads, security, entities, search, AI), and deployment guides.
|
|
109
|
+
|
|
110
|
+
Install skills for AI coding agents:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# All platform skills (security, blueprints, codegen, billing, etc.)
|
|
114
|
+
npx skills add constructive-io/constructive-skills
|
|
115
|
+
|
|
116
|
+
# Individual repo skills (pgpm, testing, CLI, search, etc.)
|
|
117
|
+
npx skills add https://github.com/constructive-io/constructive --skill pgpm
|
|
118
|
+
npx skills add https://github.com/constructive-io/constructive --skill constructive-testing
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Credits
|
|
122
|
+
|
|
123
|
+
**🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
|
|
124
|
+
|
|
125
|
+
## Disclaimer
|
|
126
|
+
|
|
127
|
+
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
|
|
128
|
+
|
|
129
|
+
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
|
package/esm/events.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AgentEvent } from '@agentic-kit/agent';
|
|
2
|
+
import type { TaskWriter, TodoItem, Transcript } from '@agentic-kit/agent-conversation';
|
|
3
|
+
/** Tool names whose arguments carry the agent's todo list. */
|
|
4
|
+
export declare const TODO_TOOL_NAMES: string[];
|
|
5
|
+
export interface TranscriptWriterOptions {
|
|
6
|
+
transcript: Transcript;
|
|
7
|
+
/** Absent when the tenant's agent module has no task surface for this run. */
|
|
8
|
+
tasks?: TaskWriter;
|
|
9
|
+
/** Tool names whose arguments carry a todo list. */
|
|
10
|
+
todoToolNames?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Translate a run's events into thread writes.
|
|
14
|
+
*
|
|
15
|
+
* Nothing here is best-effort: a failed write is kept and rethrown from
|
|
16
|
+
* `drain()`, because a transcript that silently lost the middle of a run is
|
|
17
|
+
* worse than a run that fails.
|
|
18
|
+
*/
|
|
19
|
+
export declare class TranscriptWriter {
|
|
20
|
+
private readonly options;
|
|
21
|
+
private chain;
|
|
22
|
+
private readonly pending;
|
|
23
|
+
private readonly todoToolNames;
|
|
24
|
+
private failure;
|
|
25
|
+
constructor(options: TranscriptWriterOptions);
|
|
26
|
+
/** Subscribe this writer to an agent: `agent.subscribe(writer.handler)`. */
|
|
27
|
+
readonly handler: (event: AgentEvent) => void;
|
|
28
|
+
/** Wait for every write queued so far, and surface the first that failed. */
|
|
29
|
+
drain(): Promise<void>;
|
|
30
|
+
private enqueue;
|
|
31
|
+
private apply;
|
|
32
|
+
private syncTodos;
|
|
33
|
+
}
|
|
34
|
+
/** The prose of an assistant message; thinking and tool calls are not prose. */
|
|
35
|
+
export declare function assistantText(message: {
|
|
36
|
+
content: unknown;
|
|
37
|
+
}): string;
|
|
38
|
+
/** The text of a tool result, as the transcript records it. */
|
|
39
|
+
export declare function resultText(result: {
|
|
40
|
+
content: unknown;
|
|
41
|
+
}): string;
|
|
42
|
+
/**
|
|
43
|
+
* The todo list a todo tool was called with. Returns null when the arguments do
|
|
44
|
+
* not carry one; throws when they carry a malformed one, because a todo list the
|
|
45
|
+
* tasks table cannot represent is a bug rather than an absence.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseTodos(args: Record<string, unknown>): TodoItem[] | null;
|
package/esm/events.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// `AgentEvent` → the thread.
|
|
2
|
+
//
|
|
3
|
+
// `@agentic-kit/agent` already emits everything a transcript needs, so this is a
|
|
4
|
+
// translation and nothing else: prose to a message, a tool call to a ToolPart
|
|
5
|
+
// rewritten in place as it runs, the agent's todo list to `agent_task` rows.
|
|
6
|
+
// Writes are serialized through one promise chain so the thread's order matches
|
|
7
|
+
// the run's order; `drain()` is how a composition root waits for the tail.
|
|
8
|
+
import { completeToolPart, failToolPart, toolPart } from '@agentic-kit/agent-conversation';
|
|
9
|
+
import { asOneOf, asRecord, asString } from '@constructive-io/coerce';
|
|
10
|
+
/** The text of a content block, or '' when the block carries none. */
|
|
11
|
+
const blockText = (block) => asString(asRecord(block)?.text) ?? '';
|
|
12
|
+
/** Tool names whose arguments carry the agent's todo list. */
|
|
13
|
+
export const TODO_TOOL_NAMES = ['todo_write', 'update_plan', 'write_todos'];
|
|
14
|
+
/**
|
|
15
|
+
* Translate a run's events into thread writes.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here is best-effort: a failed write is kept and rethrown from
|
|
18
|
+
* `drain()`, because a transcript that silently lost the middle of a run is
|
|
19
|
+
* worse than a run that fails.
|
|
20
|
+
*/
|
|
21
|
+
export class TranscriptWriter {
|
|
22
|
+
options;
|
|
23
|
+
chain = Promise.resolve();
|
|
24
|
+
pending = new Map();
|
|
25
|
+
todoToolNames;
|
|
26
|
+
failure;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.todoToolNames = new Set(options.todoToolNames ?? TODO_TOOL_NAMES);
|
|
30
|
+
}
|
|
31
|
+
/** Subscribe this writer to an agent: `agent.subscribe(writer.handler)`. */
|
|
32
|
+
handler = (event) => {
|
|
33
|
+
this.enqueue(() => this.apply(event));
|
|
34
|
+
};
|
|
35
|
+
/** Wait for every write queued so far, and surface the first that failed. */
|
|
36
|
+
async drain() {
|
|
37
|
+
await this.chain;
|
|
38
|
+
if (this.failure !== undefined) {
|
|
39
|
+
const failure = this.failure;
|
|
40
|
+
this.failure = undefined;
|
|
41
|
+
throw failure;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
enqueue(work) {
|
|
45
|
+
this.chain = this.chain.then(async () => {
|
|
46
|
+
if (this.failure !== undefined)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
await work();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.failure = error;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async apply(event) {
|
|
57
|
+
switch (event.type) {
|
|
58
|
+
case 'message_end': {
|
|
59
|
+
if (event.message.role !== 'assistant')
|
|
60
|
+
return;
|
|
61
|
+
const text = assistantText(event.message);
|
|
62
|
+
if (text)
|
|
63
|
+
await this.options.transcript.appendText(text);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case 'tool_execution_start': {
|
|
67
|
+
const part = toolPart({
|
|
68
|
+
toolName: event.toolName,
|
|
69
|
+
toolCallId: event.toolCallId,
|
|
70
|
+
input: event.args
|
|
71
|
+
});
|
|
72
|
+
const message = await this.options.transcript.appendToolPart(part);
|
|
73
|
+
this.pending.set(event.toolCallId, { messageId: message.id, part });
|
|
74
|
+
await this.syncTodos(event.toolName, event.args);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
case 'tool_execution_end': {
|
|
78
|
+
const call = this.pending.get(event.toolCallId);
|
|
79
|
+
if (!call) {
|
|
80
|
+
throw new Error(`tool call ${event.toolCallId} (${event.toolName}) ended without having started`);
|
|
81
|
+
}
|
|
82
|
+
this.pending.delete(event.toolCallId);
|
|
83
|
+
const output = resultText(event.result);
|
|
84
|
+
const next = event.isError
|
|
85
|
+
? failToolPart(call.part, output || 'The tool failed without a message')
|
|
86
|
+
: completeToolPart(call.part, output);
|
|
87
|
+
await this.options.transcript.updateToolPart(call.messageId, next);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case 'tool_decision_pending': {
|
|
91
|
+
// The lane's tools declare no `decision` schema — approval is the
|
|
92
|
+
// harness gate, answered through the thread. An in-band decision would
|
|
93
|
+
// stall the run forever, so it fails loudly instead.
|
|
94
|
+
throw new Error(`tool ${event.toolName} (${event.toolCallId}) asked for an in-band decision, which this runner does not serve`);
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async syncTodos(toolName, args) {
|
|
101
|
+
if (!this.options.tasks || !this.todoToolNames.has(toolName))
|
|
102
|
+
return;
|
|
103
|
+
const todos = parseTodos(args);
|
|
104
|
+
if (todos)
|
|
105
|
+
await this.options.tasks.sync(todos);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** The prose of an assistant message; thinking and tool calls are not prose. */
|
|
109
|
+
export function assistantText(message) {
|
|
110
|
+
const content = message.content;
|
|
111
|
+
const prose = asString(content);
|
|
112
|
+
if (prose)
|
|
113
|
+
return prose.trim();
|
|
114
|
+
if (!Array.isArray(content))
|
|
115
|
+
return '';
|
|
116
|
+
return content
|
|
117
|
+
.map((block) => (asRecord(block)?.type === 'text' ? blockText(block) : ''))
|
|
118
|
+
.join('')
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
|
121
|
+
/** The text of a tool result, as the transcript records it. */
|
|
122
|
+
export function resultText(result) {
|
|
123
|
+
const content = result.content;
|
|
124
|
+
const text = asString(content);
|
|
125
|
+
if (text)
|
|
126
|
+
return text;
|
|
127
|
+
if (!Array.isArray(content))
|
|
128
|
+
return '';
|
|
129
|
+
return content.map(blockText).join('').trim();
|
|
130
|
+
}
|
|
131
|
+
const TODO_STATUSES = ['pending', 'in_progress', 'completed', 'failed'];
|
|
132
|
+
/**
|
|
133
|
+
* The todo list a todo tool was called with. Returns null when the arguments do
|
|
134
|
+
* not carry one; throws when they carry a malformed one, because a todo list the
|
|
135
|
+
* tasks table cannot represent is a bug rather than an absence.
|
|
136
|
+
*/
|
|
137
|
+
export function parseTodos(args) {
|
|
138
|
+
const raw = args.todos ?? args.items ?? args.plan;
|
|
139
|
+
if (raw === undefined || raw === null)
|
|
140
|
+
return null;
|
|
141
|
+
if (!Array.isArray(raw))
|
|
142
|
+
throw new Error('todo tool called with a non-list todo argument');
|
|
143
|
+
return raw.map((entry, index) => {
|
|
144
|
+
const item = asRecord(entry);
|
|
145
|
+
if (!item)
|
|
146
|
+
throw new Error(`todo ${index} is not an object`);
|
|
147
|
+
const description = asString(item.description ?? item.content ?? item.title ?? item.step);
|
|
148
|
+
if (!description)
|
|
149
|
+
throw new Error(`todo ${index} carries no description`);
|
|
150
|
+
const status = asOneOf(item.status ?? 'pending', TODO_STATUSES);
|
|
151
|
+
if (!status) {
|
|
152
|
+
throw new Error(`todo ${index} has status "${String(item.status)}", which agent_task does not model`);
|
|
153
|
+
}
|
|
154
|
+
const error = asString(item.error);
|
|
155
|
+
return {
|
|
156
|
+
description: description.trim(),
|
|
157
|
+
status,
|
|
158
|
+
...(error ? { error } : {})
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
package/esm/gate.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Inbox, ToolPart, Transcript } from '@agentic-kit/agent-conversation';
|
|
2
|
+
import type { GateHost } from '@agentic-kit/harness';
|
|
3
|
+
export interface ThreadGateHostOptions {
|
|
4
|
+
transcript: Transcript;
|
|
5
|
+
inbox: Inbox;
|
|
6
|
+
/** How long a pending approval waits before it is treated as declined. */
|
|
7
|
+
approvalTimeoutMs?: number;
|
|
8
|
+
/** Approval ids, as a value — a suite passes a counter. */
|
|
9
|
+
newApprovalId?: () => string;
|
|
10
|
+
/** Called when the human asked the run to stop while an approval was pending. */
|
|
11
|
+
onCancel?: (reason: string | undefined) => void;
|
|
12
|
+
/** Called with every tool part the gate writes, so a caller can trace the run. */
|
|
13
|
+
onToolPart?: (part: ToolPart) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_APPROVAL_TIMEOUT_MS: number;
|
|
16
|
+
/**
|
|
17
|
+
* The harness's `GateHost` plus the drain its one synchronous method needs.
|
|
18
|
+
*
|
|
19
|
+
* `notifyToolSkipped` returns void, so its transcript write cannot be awaited
|
|
20
|
+
* where it is called. It is queued instead, and `drain()` — awaited by the
|
|
21
|
+
* composition root once the run ends — rethrows the first failure, so a write
|
|
22
|
+
* that fails still fails the Job rather than disappearing into a `catch`.
|
|
23
|
+
*/
|
|
24
|
+
export type ThreadGateHost = GateHost & {
|
|
25
|
+
drain(): Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A `GateHost` whose UI is the thread.
|
|
29
|
+
*
|
|
30
|
+
* The pending call is one message whose single ToolPart is rewritten in place as
|
|
31
|
+
* the decision arrives, so the thread reads as a conversation and the state
|
|
32
|
+
* machine in `@agentic-kit/agent-conversation` is the only place the
|
|
33
|
+
* lifecycle is enforced.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createThreadGateHost(options: ThreadGateHostOptions): ThreadGateHost;
|
package/esm/gate.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The GateHost the harness already defines, answered by the conversation.
|
|
2
|
+
//
|
|
3
|
+
// `@agentic-kit/harness` owns *what* is gated (`MUTATING_DB_TOOLS`), the prompt
|
|
4
|
+
// wording (`buildConfirmPrompt`) and the decline guard; a host only supplies the
|
|
5
|
+
// three capabilities in `GateHost`. Desktop answers `confirmTool` with a dialog.
|
|
6
|
+
// A Job answers it with a message: it writes the tool call into the thread at
|
|
7
|
+
// `approval-requested` and waits for the human to echo the part back at
|
|
8
|
+
// `approval-responded`. That is the same contract, resolved asynchronously —
|
|
9
|
+
// which is exactly why the runner reports `hasUI: true`. Reporting false would
|
|
10
|
+
// tell the gate no decision can ever be obtained and block every mutating tool.
|
|
11
|
+
import { denyToolPart, failToolPart, isApprovalEvent, requestApproval, respondToApproval, toolPart } from '@agentic-kit/agent-conversation';
|
|
12
|
+
export const DEFAULT_APPROVAL_TIMEOUT_MS = 15 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* A `GateHost` whose UI is the thread.
|
|
15
|
+
*
|
|
16
|
+
* The pending call is one message whose single ToolPart is rewritten in place as
|
|
17
|
+
* the decision arrives, so the thread reads as a conversation and the state
|
|
18
|
+
* machine in `@agentic-kit/agent-conversation` is the only place the
|
|
19
|
+
* lifecycle is enforced.
|
|
20
|
+
*/
|
|
21
|
+
export function createThreadGateHost(options) {
|
|
22
|
+
const { transcript, inbox, approvalTimeoutMs = DEFAULT_APPROVAL_TIMEOUT_MS, newApprovalId = () => `approval-${Math.random().toString(36).slice(2, 10)}`, onCancel, onToolPart } = options;
|
|
23
|
+
const emit = (part) => {
|
|
24
|
+
onToolPart?.(part);
|
|
25
|
+
return part;
|
|
26
|
+
};
|
|
27
|
+
const confirmTool = async (toolCallId, title, message, preview) => {
|
|
28
|
+
const pending = toolPart({
|
|
29
|
+
toolName: 'confirm',
|
|
30
|
+
toolCallId,
|
|
31
|
+
input: { title, message, ...(preview ? { preview } : {}) }
|
|
32
|
+
});
|
|
33
|
+
const requested = emit(requestApproval(pending, newApprovalId()));
|
|
34
|
+
const written = await transcript.appendToolPart(requested);
|
|
35
|
+
const outcome = await inbox.waitFor(isApprovalEvent, { timeoutMs: approvalTimeoutMs });
|
|
36
|
+
if (outcome.event && outcome.event.toolCallId === toolCallId) {
|
|
37
|
+
// Two writes on a decline, on purpose: `approval-responded` is the human's
|
|
38
|
+
// answer and `output-denied` is the call's outcome, and the transcript is
|
|
39
|
+
// read as a history — collapsing them would lose who declined and why.
|
|
40
|
+
const responded = respondToApproval(requested, {
|
|
41
|
+
approved: outcome.event.approved,
|
|
42
|
+
reason: outcome.event.reason
|
|
43
|
+
});
|
|
44
|
+
await transcript.updateToolPart(written.id, emit(responded));
|
|
45
|
+
if (!outcome.event.approved) {
|
|
46
|
+
await transcript.updateToolPart(written.id, emit(denyToolPart(responded, outcome.event.reason ?? 'Declined by the user')));
|
|
47
|
+
}
|
|
48
|
+
return outcome.event.approved;
|
|
49
|
+
}
|
|
50
|
+
if (outcome.event) {
|
|
51
|
+
// A decision arrived for a call this gate is not waiting on. The gate is
|
|
52
|
+
// serial — one pending confirm at a time — so this is a malformed client,
|
|
53
|
+
// not a race, and silently dropping it would strand the run.
|
|
54
|
+
throw new Error(`approval for tool call ${outcome.event.toolCallId} arrived while ${toolCallId} was pending`);
|
|
55
|
+
}
|
|
56
|
+
if (outcome.cancelled) {
|
|
57
|
+
onCancel?.(outcome.cancelled.reason);
|
|
58
|
+
await transcript.updateToolPart(written.id, emit(denyToolPart(requested, outcome.cancelled.reason ?? 'Cancelled by the user')));
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
await transcript.updateToolPart(written.id, emit(failToolPart(requested, `No decision within ${Math.round(approvalTimeoutMs / 1000)}s`)));
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
// Queued notices, serialized so the transcript keeps the order the gate saw,
|
|
65
|
+
// and their first failure, kept for `drain()` to rethrow.
|
|
66
|
+
let queue = Promise.resolve();
|
|
67
|
+
let failure;
|
|
68
|
+
return {
|
|
69
|
+
hasUI: true,
|
|
70
|
+
confirmTool,
|
|
71
|
+
notifyToolSkipped: (toolCallId) => {
|
|
72
|
+
queue = queue.then(async () => {
|
|
73
|
+
try {
|
|
74
|
+
await transcript.appendText(`Skipped a repeat of a declined tool call (${toolCallId}).`);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
failure ??= error;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
drain: async () => {
|
|
82
|
+
await queue;
|
|
83
|
+
if (failure !== undefined) {
|
|
84
|
+
throw new Error(`failed to write a skipped-tool notice to the thread: ${failure instanceof Error ? failure.message : String(failure)}`, { cause: failure });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
package/esm/host.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ToolsHost } from '@agentic-kit/pi/host';
|
|
2
|
+
export interface PiHostValues {
|
|
3
|
+
userId: string;
|
|
4
|
+
accessToken: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
apiEndpoint?: string;
|
|
7
|
+
modulesEndpoint?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A `ToolsHost` built from values.
|
|
11
|
+
*
|
|
12
|
+
* The desktop host reads an Electron singleton; a Job has a token and two
|
|
13
|
+
* endpoints, and nothing else. It offers no preview token, no data-auth broker
|
|
14
|
+
* and no step-up, so the data-plane tools decline cleanly rather than half-work,
|
|
15
|
+
* and no `deliverSecret`, so `create_api_key` refuses to mint a secret that has
|
|
16
|
+
* nowhere safe to go.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createPiToolsHost(values: PiHostValues): ToolsHost;
|
package/esm/host.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The `ToolsHost` a Job can honestly provide.
|
|
2
|
+
/**
|
|
3
|
+
* A `ToolsHost` built from values.
|
|
4
|
+
*
|
|
5
|
+
* The desktop host reads an Electron singleton; a Job has a token and two
|
|
6
|
+
* endpoints, and nothing else. It offers no preview token, no data-auth broker
|
|
7
|
+
* and no step-up, so the data-plane tools decline cleanly rather than half-work,
|
|
8
|
+
* and no `deliverSecret`, so `create_api_key` refuses to mint a secret that has
|
|
9
|
+
* nowhere safe to go.
|
|
10
|
+
*/
|
|
11
|
+
export function createPiToolsHost(values) {
|
|
12
|
+
if (!values.accessToken)
|
|
13
|
+
throw new Error('a pi tools host needs an access token');
|
|
14
|
+
return {
|
|
15
|
+
account: () => ({
|
|
16
|
+
userId: values.userId,
|
|
17
|
+
accessToken: values.accessToken,
|
|
18
|
+
...(values.apiKey ? { apiKey: values.apiKey } : {})
|
|
19
|
+
}),
|
|
20
|
+
backendConfig: () => ({
|
|
21
|
+
...(values.apiEndpoint ? { apiEndpoint: values.apiEndpoint } : {}),
|
|
22
|
+
...(values.modulesEndpoint ? { modulesEndpoint: values.modulesEndpoint } : {})
|
|
23
|
+
}),
|
|
24
|
+
signInHint: 'This run was launched without a usable platform token.'
|
|
25
|
+
};
|
|
26
|
+
}
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { TranscriptWriterOptions } from './events';
|
|
2
|
+
export { assistantText, parseTodos, resultText, TODO_TOOL_NAMES, TranscriptWriter } from './events';
|
|
3
|
+
export type { ThreadGateHost, ThreadGateHostOptions } from './gate';
|
|
4
|
+
export { createThreadGateHost, DEFAULT_APPROVAL_TIMEOUT_MS } from './gate';
|
|
5
|
+
export type { PiHostValues } from './host';
|
|
6
|
+
export { createPiToolsHost } from './host';
|
|
7
|
+
export type { MeteredIdentity, MeteredModel, MeteredModelOptions } from './model';
|
|
8
|
+
export { createMeteredModel, gatewayApiRoot, meteringHeaders } from './model';
|
|
9
|
+
export type { LoadPersonaInput, PersonaConfig, PersonaRow, PersonaSelection, SelectPersonaInput, SkillResource } from './persona';
|
|
10
|
+
export { loadPersona, loadPersonaSkills, PersonaModelUnresolvedError, PersonaNotFoundError, selectPersona, UnknownPersonaToolError } from './persona';
|
|
11
|
+
export type { ProjectContextRequest } from './project-context';
|
|
12
|
+
export { isInside, materializeProjectContext, ProjectContextInsideWorkTreeError } from './project-context';
|
|
13
|
+
export type { GatedToolset, GatedToolsetOptions } from './tools';
|
|
14
|
+
export { CLONE_GATE_DEPS, createGatedToolset } from './tools';
|
|
15
|
+
export type { WorkspaceToolsOptions } from './workspace-tools';
|
|
16
|
+
export { createWorkspaceTools, DEFAULT_COMMAND_TIMEOUT_MS, MAX_READ_BYTES, OutsideWorkspaceError, WORKSPACE_TOOL_NAMES } from './workspace-tools';
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @agentic-kit/pi-host — the host half of a headless agent run: the
|
|
2
|
+
// harness's `GateHost` answered by the conversation thread, persona selection,
|
|
3
|
+
// the gated toolset, and `AgentEvent` → transcript. Every dependency arrives as
|
|
4
|
+
// a value; nothing here knows what a `ctx` is.
|
|
5
|
+
export { assistantText, parseTodos, resultText, TODO_TOOL_NAMES, TranscriptWriter } from './events';
|
|
6
|
+
export { createThreadGateHost, DEFAULT_APPROVAL_TIMEOUT_MS } from './gate';
|
|
7
|
+
export { createPiToolsHost } from './host';
|
|
8
|
+
export { createMeteredModel, gatewayApiRoot, meteringHeaders } from './model';
|
|
9
|
+
export { loadPersona, loadPersonaSkills, PersonaModelUnresolvedError, PersonaNotFoundError, selectPersona, UnknownPersonaToolError } from './persona';
|
|
10
|
+
export { isInside, materializeProjectContext, ProjectContextInsideWorkTreeError } from './project-context';
|
|
11
|
+
export { CLONE_GATE_DEPS, createGatedToolset } from './tools';
|
|
12
|
+
export { createWorkspaceTools, DEFAULT_COMMAND_TIMEOUT_MS, MAX_READ_BYTES, OutsideWorkspaceError, WORKSPACE_TOOL_NAMES } from './workspace-tools';
|
package/esm/model.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AssistantMessageEventStream, Context, ModelDescriptor, StreamOptions } from '@agentic-kit/chat';
|
|
2
|
+
/** Who the inference is billed to. Mirrors the http runtime's `AgentHeaders`. */
|
|
3
|
+
export interface MeteredIdentity {
|
|
4
|
+
databaseId: string;
|
|
5
|
+
entityId?: string | null;
|
|
6
|
+
organizationId?: string | null;
|
|
7
|
+
actorId?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface MeteredModelOptions extends MeteredIdentity {
|
|
10
|
+
/** `AGENTIC_SERVER_URL` — the gateway's root, with or without `/v1`. */
|
|
11
|
+
agenticServerUrl: string;
|
|
12
|
+
/** The model id the persona selected. */
|
|
13
|
+
model: string;
|
|
14
|
+
/** Injectable for tests; defaults to the runtime's `fetch`. */
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
export interface MeteredModel {
|
|
18
|
+
model: ModelDescriptor;
|
|
19
|
+
/** `Agent`'s `streamFn`. */
|
|
20
|
+
streamFn: (model: ModelDescriptor, context: Context, options?: StreamOptions) => AssistantMessageEventStream;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The identity headers `agentic-server` meters against. `X-Database-Id` is
|
|
24
|
+
* required; the billing entity falls back the way the node runtime falls back,
|
|
25
|
+
* so a run launched without an entity still bills the actor's personal org.
|
|
26
|
+
*/
|
|
27
|
+
export declare function meteringHeaders(identity: MeteredIdentity): Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* The gateway's OpenAI-compatible api root, `<host>/v1`, whichever form the
|
|
30
|
+
* projection took.
|
|
31
|
+
*
|
|
32
|
+
* `AGENTIC_SERVER_URL` is documented as the bare root, but an operator reading a
|
|
33
|
+
* provider's own docs sets it with `/v1` — and `resolveMeteredGateway` accepts
|
|
34
|
+
* both, so the two halves of one stack disagreed: appending `/v1` unconditionally
|
|
35
|
+
* posted to `<host>/v1/v1/chat/completions`, which the gateway answers with a 404
|
|
36
|
+
* that surfaces as "no assistant message" naming no url.
|
|
37
|
+
*/
|
|
38
|
+
export declare function gatewayApiRoot(agenticServerUrl: string): string;
|
|
39
|
+
export declare function createMeteredModel(options: MeteredModelOptions): MeteredModel;
|