arcie 0.1.3 → 0.1.5
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 +34 -6
- package/dist/agent/index.d.ts +6 -3
- package/dist/agent/index.js +6 -6
- package/dist/agent/index.js.map +1 -1
- package/dist/auth/index.d.ts +6 -5
- package/dist/auth/index.js +8 -17
- package/dist/auth/index.js.map +1 -1
- package/dist/build-TWNCPXBU.js +82 -0
- package/dist/build-TWNCPXBU.js.map +1 -0
- package/dist/channels/index.d.ts +8 -5
- package/dist/channels/index.js +10 -12
- package/dist/channels/index.js.map +1 -1
- package/dist/chunk-3CNQ6HQV.js +12 -0
- package/dist/chunk-3CNQ6HQV.js.map +1 -0
- package/dist/chunk-4L7WVCTU.js +12 -0
- package/dist/chunk-4L7WVCTU.js.map +1 -0
- package/dist/chunk-4WSILP75.js +1774 -0
- package/dist/chunk-4WSILP75.js.map +1 -0
- package/dist/chunk-6XETPLIF.js +176 -0
- package/dist/chunk-6XETPLIF.js.map +1 -0
- package/dist/chunk-CKHBEZCT.js +24 -0
- package/dist/chunk-CKHBEZCT.js.map +1 -0
- package/dist/chunk-FQ7KDNV5.js +12 -0
- package/dist/chunk-FQ7KDNV5.js.map +1 -0
- package/dist/chunk-HVBDOHTL.js +148 -0
- package/dist/chunk-HVBDOHTL.js.map +1 -0
- package/dist/chunk-HWD2Z2CH.js +742 -0
- package/dist/chunk-HWD2Z2CH.js.map +1 -0
- package/dist/chunk-KVSX4MLK.js +139 -0
- package/dist/chunk-KVSX4MLK.js.map +1 -0
- package/dist/chunk-L3BTL5FM.js +39 -0
- package/dist/chunk-L3BTL5FM.js.map +1 -0
- package/dist/chunk-LQC65UHE.js +34 -0
- package/dist/chunk-LQC65UHE.js.map +1 -0
- package/dist/chunk-N5R3TEEZ.js +15 -0
- package/dist/chunk-N5R3TEEZ.js.map +1 -0
- package/dist/chunk-PHTJLJOV.js +25 -0
- package/dist/chunk-PHTJLJOV.js.map +1 -0
- package/dist/chunk-QE46QFID.js +92 -0
- package/dist/chunk-QE46QFID.js.map +1 -0
- package/dist/chunk-SGWQZZ2A.js +54 -0
- package/dist/chunk-SGWQZZ2A.js.map +1 -0
- package/dist/chunk-WAD7VLNI.js +51 -0
- package/dist/chunk-WAD7VLNI.js.map +1 -0
- package/dist/chunk-XOFWQUOO.js +30 -0
- package/dist/chunk-XOFWQUOO.js.map +1 -0
- package/dist/chunk-ZJNHQXMO.js +20 -0
- package/dist/chunk-ZJNHQXMO.js.map +1 -0
- package/dist/cli/index.d.ts +7 -2
- package/dist/cli/index.js +171 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/context/index.d.ts +14 -11
- package/dist/context/index.js +22 -37
- package/dist/context/index.js.map +1 -1
- package/dist/dev-ZZDVOWUZ.js +1967 -0
- package/dist/dev-ZZDVOWUZ.js.map +1 -0
- package/dist/discover/index.d.ts +17 -7
- package/dist/discover/index.js +6 -132
- package/dist/discover/index.js.map +1 -1
- package/dist/hooks/index.d.ts +6 -3
- package/dist/hooks/index.js +6 -6
- package/dist/hooks/index.js.map +1 -1
- package/dist/index-CElLRVTP.d.ts +136 -0
- package/dist/index.d.ts +297 -18
- package/dist/index.js +160 -13
- package/dist/index.js.map +1 -1
- package/dist/init-5F7CIVLN.js +822 -0
- package/dist/init-5F7CIVLN.js.map +1 -0
- package/dist/instructions/index.d.ts +7 -4
- package/dist/instructions/index.js +8 -32
- package/dist/instructions/index.js.map +1 -1
- package/dist/protocol/events.d.ts +47 -39
- package/dist/protocol/events.js +46 -45
- package/dist/protocol/events.js.map +1 -1
- package/dist/runner/index.d.ts +4 -18
- package/dist/runner/index.js +11 -277
- package/dist/runner/index.js.map +1 -1
- package/dist/scaffold-web-chat-OQL5C2GK.js +7 -0
- package/dist/scaffold-web-chat-OQL5C2GK.js.map +1 -0
- package/dist/schedules/index.d.ts +6 -3
- package/dist/schedules/index.js +6 -6
- package/dist/schedules/index.js.map +1 -1
- package/dist/skills/index.d.ts +7 -4
- package/dist/skills/index.js +8 -27
- package/dist/skills/index.js.map +1 -1
- package/dist/tools/index.d.ts +13 -3
- package/dist/tools/index.js +8 -9
- package/dist/tools/index.js.map +1 -1
- package/dist/types-DwxwdSa2.d.ts +151 -0
- package/docs/getting-started.md +2 -2
- package/docs/index.md +3 -2
- package/docs/project-layout.md +2 -2
- package/docs/subagents.md +95 -0
- package/package.json +15 -10
- package/templates/default/.env.local +12 -0
- package/templates/default/agent/agent.ts +2 -2
- package/templates/default/package.json +3 -3
- package/templates/web-chat/.env.local.example +4 -0
- package/templates/web-chat/README.md +51 -0
- package/templates/web-chat/app/api/chat/route.ts +43 -0
- package/templates/web-chat/app/globals.css +55 -0
- package/templates/web-chat/app/layout.tsx +15 -0
- package/templates/web-chat/app/page.tsx +9 -0
- package/templates/web-chat/components/chat.tsx +196 -0
- package/templates/web-chat/components/input-bar.tsx +74 -0
- package/templates/web-chat/components/message.tsx +63 -0
- package/templates/web-chat/components/tool-call.tsx +79 -0
- package/templates/web-chat/components/ui/button.tsx +46 -0
- package/templates/web-chat/components/ui/input.tsx +19 -0
- package/templates/web-chat/components.json +20 -0
- package/templates/web-chat/lib/stream.ts +41 -0
- package/templates/web-chat/lib/types.ts +61 -0
- package/templates/web-chat/lib/utils.ts +6 -0
- package/templates/web-chat/next.config.mjs +4 -0
- package/templates/web-chat/package.json +32 -0
- package/templates/web-chat/postcss.config.mjs +6 -0
- package/templates/web-chat/tailwind.config.ts +59 -0
- package/templates/web-chat/tsconfig.json +23 -0
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/channels/index.d.ts.map +0 -1
- package/dist/cli/banner.d.ts +0 -2
- package/dist/cli/banner.d.ts.map +0 -1
- package/dist/cli/banner.js +0 -10
- package/dist/cli/banner.js.map +0 -1
- package/dist/cli/build.d.ts +0 -5
- package/dist/cli/build.d.ts.map +0 -1
- package/dist/cli/build.js +0 -54
- package/dist/cli/build.js.map +0 -1
- package/dist/cli/dev.d.ts +0 -5
- package/dist/cli/dev.d.ts.map +0 -1
- package/dist/cli/dev.js +0 -74
- package/dist/cli/dev.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/init.d.ts +0 -4
- package/dist/cli/init.d.ts.map +0 -1
- package/dist/cli/init.js +0 -75
- package/dist/cli/init.js.map +0 -1
- package/dist/context/index.d.ts.map +0 -1
- package/dist/discover/index.d.ts.map +0 -1
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/instructions/index.d.ts.map +0 -1
- package/dist/loader.d.ts +0 -7
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -102
- package/dist/loader.js.map +0 -1
- package/dist/protocol/events.d.ts.map +0 -1
- package/dist/runner/index.d.ts.map +0 -1
- package/dist/schedules/index.d.ts.map +0 -1
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -118
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Subagents
|
|
2
|
+
|
|
3
|
+
A subagent is a child agent that the orchestrator delegates a focused subtask to.
|
|
4
|
+
Each one runs in its own **fresh, isolated session** — it never sees the parent's
|
|
5
|
+
conversation history. The parent passes everything the child needs through a single
|
|
6
|
+
`message` and gets a result back. This keeps the orchestrator's context small and
|
|
7
|
+
each specialist sharply scoped.
|
|
8
|
+
|
|
9
|
+
## Two kinds
|
|
10
|
+
|
|
11
|
+
- **Declared subagents** — specialists you define under `agent/subagents/<id>/`.
|
|
12
|
+
Use one when the child needs a different prompt, a narrower tool surface, or its
|
|
13
|
+
own runtime context.
|
|
14
|
+
- **The built-in `agent` tool** — every agent can spawn a copy of itself for a
|
|
15
|
+
focused subtask. The copy inherits the parent's instructions, tools, and skills,
|
|
16
|
+
but starts with fresh state.
|
|
17
|
+
|
|
18
|
+
If the agent can keep its own identity and just needs an optional procedure, reach
|
|
19
|
+
for a skill instead of a subagent.
|
|
20
|
+
|
|
21
|
+
## Defining a subagent
|
|
22
|
+
|
|
23
|
+
A subagent is a directory that looks like a small agent:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
agent/subagents/researcher/
|
|
27
|
+
├── agent.ts # required — defineAgent({ model, description })
|
|
28
|
+
├── instructions.md # optional — the subagent's system prompt
|
|
29
|
+
├── tools/ # optional — the subagent's own tools
|
|
30
|
+
└── skills/ # optional — the subagent's own knowledge
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// agent/subagents/researcher/agent.ts
|
|
35
|
+
import { defineAgent } from "arcie";
|
|
36
|
+
|
|
37
|
+
export default defineAgent({
|
|
38
|
+
model: "claude-sonnet-4-5",
|
|
39
|
+
description: "Investigate ambiguous questions before the parent agent responds.",
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The directory name (`researcher`) becomes the tool name the orchestrator calls.
|
|
44
|
+
|
|
45
|
+
### `description` is required
|
|
46
|
+
|
|
47
|
+
Unlike the root agent, a subagent **must** declare a `description` — it's how the
|
|
48
|
+
orchestrator decides when to delegate. Leaving it out is a hard error at load time:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Subagent "researcher" must declare a description in agent.ts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A subagent directory with no `agent.ts` is reported by `arcie dev` / `arcie build` as
|
|
55
|
+
`SUBAGENT_MISSING_CONFIG`.
|
|
56
|
+
|
|
57
|
+
## Isolation
|
|
58
|
+
|
|
59
|
+
Declared subagents are isolated. They **inherit nothing** from the parent — absent
|
|
60
|
+
slots fall back to the framework default, not to the root agent's version. If two
|
|
61
|
+
subagents need the same procedure, copy it under each one's `skills/`.
|
|
62
|
+
|
|
63
|
+
This isolation is deliberate: each delegation spins up its own session, so the
|
|
64
|
+
subagent's work never pollutes the parent's context, and the parent only ever sees
|
|
65
|
+
the subagent's final result.
|
|
66
|
+
|
|
67
|
+
## How delegation works
|
|
68
|
+
|
|
69
|
+
The orchestrator perceives subagents as tools. Both declared subagents and the
|
|
70
|
+
built-in `agent` tool expose the same interface:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
{
|
|
74
|
+
message: string; // all the context the child needs (it sees nothing else)
|
|
75
|
+
outputSchema?: object; // when set, the child returns structured output
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When `outputSchema` is provided, the subagent runs in **task mode** and returns
|
|
80
|
+
structured output as the tool result — which is what makes fan-out / map-reduce
|
|
81
|
+
orchestration possible.
|
|
82
|
+
|
|
83
|
+
Each delegation emits two control-plane events in the parent's stream:
|
|
84
|
+
|
|
85
|
+
- `subagent.called` — `{ name, callId, childSessionId, turnId }`
|
|
86
|
+
- `subagent.completed` — `{ name, callId, output }`
|
|
87
|
+
|
|
88
|
+
## Status
|
|
89
|
+
|
|
90
|
+
Authoring and loading work today: subagents are discovered, validated (including the
|
|
91
|
+
`description` requirement), and loaded into the agent manifest with their own
|
|
92
|
+
instructions and tools — `arcie build` lists them under `subagents` in the manifest.
|
|
93
|
+
The runtime orchestration loop — the model invoking a subagent, spawning the isolated
|
|
94
|
+
child session, and feeding the result back into the parent turn — is being wired
|
|
95
|
+
through the Cencori Sessions API.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcie",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"description": "The electronic line — build agents at the speed of light.",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"arcie": "dist/cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./channels": "./dist/channels/index.js",
|
|
23
23
|
"./schedules": "./dist/schedules/index.js",
|
|
24
24
|
"./context": "./dist/context/index.js",
|
|
25
|
+
"./memory": "./dist/memory/index.js",
|
|
25
26
|
"./agent": "./dist/agent/index.js",
|
|
26
27
|
"./runner": "./dist/runner/index.js",
|
|
27
28
|
"./auth": "./dist/auth/index.js",
|
|
@@ -32,19 +33,21 @@
|
|
|
32
33
|
"*": {
|
|
33
34
|
"*": [
|
|
34
35
|
"./dist/*/index.d.ts",
|
|
36
|
+
"./dist/*.d.ts",
|
|
35
37
|
"./dist/index.d.ts"
|
|
36
38
|
]
|
|
37
39
|
}
|
|
38
40
|
},
|
|
39
41
|
"scripts": {
|
|
40
|
-
"build": "
|
|
41
|
-
"dev": "
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
42
45
|
"prepublishOnly": "npm run build",
|
|
43
46
|
"test": "vitest run",
|
|
44
47
|
"test:watch": "vitest"
|
|
45
48
|
},
|
|
46
49
|
"keywords": [
|
|
47
|
-
"
|
|
50
|
+
"arcie",
|
|
48
51
|
"ai",
|
|
49
52
|
"agent",
|
|
50
53
|
"framework",
|
|
@@ -55,10 +58,11 @@
|
|
|
55
58
|
"license": "MIT",
|
|
56
59
|
"repository": {
|
|
57
60
|
"type": "git",
|
|
58
|
-
"url": "git+https://github.com/cencori/
|
|
61
|
+
"url": "git+https://github.com/cencori/arcie.git"
|
|
59
62
|
},
|
|
60
|
-
"homepage": "https://cencori.com/
|
|
63
|
+
"homepage": "https://cencori.com/arcie",
|
|
61
64
|
"dependencies": {
|
|
65
|
+
"@libsql/client": "^0.17.4",
|
|
62
66
|
"chalk": "^5.3.0",
|
|
63
67
|
"commander": "^12.0.0",
|
|
64
68
|
"ora": "^8.0.1",
|
|
@@ -66,8 +70,9 @@
|
|
|
66
70
|
"zod-to-json-schema": "^3.25.2"
|
|
67
71
|
},
|
|
68
72
|
"devDependencies": {
|
|
69
|
-
"@types/node": "^
|
|
70
|
-
"
|
|
71
|
-
"
|
|
73
|
+
"@types/node": "^26.0.0",
|
|
74
|
+
"tsup": "^8.5.1",
|
|
75
|
+
"typescript": "^6.0.3",
|
|
76
|
+
"vitest": "^4.1.9"
|
|
72
77
|
}
|
|
73
78
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Cencori AI Gateway
|
|
2
|
+
# CENCORI_API_KEY=
|
|
3
|
+
# CENCORI_PROJECT_ID=
|
|
4
|
+
|
|
5
|
+
# Or bring your own provider (local dev server needs at least one)
|
|
6
|
+
# OPENAI_API_KEY=
|
|
7
|
+
# ANTHROPIC_API_KEY=
|
|
8
|
+
# GROQ_API_KEY=
|
|
9
|
+
# DEEPSEEK_API_KEY=
|
|
10
|
+
# MISTRAL_API_KEY=
|
|
11
|
+
# GOOGLE_API_KEY=
|
|
12
|
+
# TOGETHER_API_KEY=
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { defineAgent } from "
|
|
1
|
+
import { defineAgent } from "arcie";
|
|
2
2
|
|
|
3
3
|
export default defineAgent({
|
|
4
4
|
model: "claude-sonnet-4-5",
|
|
5
5
|
name: "my-agent",
|
|
6
|
-
description: "An
|
|
6
|
+
description: "An Arcie agent powered by Cencori.",
|
|
7
7
|
cencori: {
|
|
8
8
|
project: process.env.CENCORI_PROJECT_ID,
|
|
9
9
|
billing: {
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "
|
|
8
|
-
"build": "
|
|
7
|
+
"dev": "arcie dev",
|
|
8
|
+
"build": "arcie build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"
|
|
11
|
+
"arcie": "^0.1.2",
|
|
12
12
|
"zod": "^3.23.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# web-chat
|
|
2
|
+
|
|
3
|
+
A Next.js chat UI for your arcie agent. Talks to arcie's HTTP server through a
|
|
4
|
+
server route so the browser never sees the agent's endpoint directly.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
cp .env.local.example .env.local
|
|
10
|
+
# edit ARCIE_URL if your agent isn't on http://localhost:3000
|
|
11
|
+
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Open http://localhost:3001 in a browser. In another terminal, start your agent:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# from the parent agent directory
|
|
20
|
+
arcie dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Deploy
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Vercel
|
|
27
|
+
npx vercel
|
|
28
|
+
|
|
29
|
+
# Any Node host
|
|
30
|
+
npm run build && npm start
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Set `ARCIE_URL` in the host's environment to point at your deployed agent's
|
|
34
|
+
HTTP endpoint.
|
|
35
|
+
|
|
36
|
+
## Customize
|
|
37
|
+
|
|
38
|
+
Everything is yours to edit:
|
|
39
|
+
|
|
40
|
+
- `app/page.tsx` — the chat page
|
|
41
|
+
- `components/chat.tsx` — the main chat container
|
|
42
|
+
- `components/message.tsx` — user + assistant message bubbles
|
|
43
|
+
- `components/tool-call.tsx` — how tool calls render
|
|
44
|
+
- `app/globals.css` — theme tokens (colors, radius)
|
|
45
|
+
- `tailwind.config.ts` — Tailwind config
|
|
46
|
+
|
|
47
|
+
Add more shadcn components:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx shadcn@latest add card avatar
|
|
51
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
|
|
3
|
+
export const runtime = "nodejs";
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
const ARCIE_URL = process.env.ARCIE_URL ?? "http://localhost:3000";
|
|
7
|
+
|
|
8
|
+
export async function POST(req: NextRequest) {
|
|
9
|
+
const { message } = (await req.json()) as { message: string };
|
|
10
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
11
|
+
return new Response("message required", { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let upstream: Response;
|
|
15
|
+
try {
|
|
16
|
+
upstream = await fetch(ARCIE_URL, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify({ message, stream: true }),
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return Response.json(
|
|
23
|
+
{
|
|
24
|
+
error: "Could not reach arcie server",
|
|
25
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
26
|
+
hint: `Is arcie running at ${ARCIE_URL}? Start it with \`arcie dev\` in the agent directory.`,
|
|
27
|
+
},
|
|
28
|
+
{ status: 502 },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!upstream.ok || upstream.body === null) {
|
|
33
|
+
return new Response(await upstream.text(), { status: upstream.status });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Response(upstream.body, {
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/x-ndjson",
|
|
39
|
+
"Cache-Control": "no-cache, no-transform",
|
|
40
|
+
Connection: "keep-alive",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 240 10% 3.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 240 10% 3.9%;
|
|
11
|
+
--primary: 240 5.9% 10%;
|
|
12
|
+
--primary-foreground: 0 0% 98%;
|
|
13
|
+
--secondary: 240 4.8% 95.9%;
|
|
14
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
15
|
+
--muted: 240 4.8% 95.9%;
|
|
16
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
17
|
+
--accent: 240 4.8% 95.9%;
|
|
18
|
+
--accent-foreground: 240 5.9% 10%;
|
|
19
|
+
--destructive: 0 84.2% 60.2%;
|
|
20
|
+
--destructive-foreground: 0 0% 98%;
|
|
21
|
+
--border: 240 5.9% 90%;
|
|
22
|
+
--input: 240 5.9% 90%;
|
|
23
|
+
--ring: 240 5.9% 10%;
|
|
24
|
+
--radius: 0.5rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.dark {
|
|
28
|
+
--background: 240 10% 3.9%;
|
|
29
|
+
--foreground: 0 0% 98%;
|
|
30
|
+
--card: 240 10% 3.9%;
|
|
31
|
+
--card-foreground: 0 0% 98%;
|
|
32
|
+
--primary: 0 0% 98%;
|
|
33
|
+
--primary-foreground: 240 5.9% 10%;
|
|
34
|
+
--secondary: 240 3.7% 15.9%;
|
|
35
|
+
--secondary-foreground: 0 0% 98%;
|
|
36
|
+
--muted: 240 3.7% 15.9%;
|
|
37
|
+
--muted-foreground: 240 5% 64.9%;
|
|
38
|
+
--accent: 240 3.7% 15.9%;
|
|
39
|
+
--accent-foreground: 0 0% 98%;
|
|
40
|
+
--destructive: 0 62.8% 30.6%;
|
|
41
|
+
--destructive-foreground: 0 0% 98%;
|
|
42
|
+
--border: 240 3.7% 15.9%;
|
|
43
|
+
--input: 240 3.7% 15.9%;
|
|
44
|
+
--ring: 240 4.9% 83.9%;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@layer base {
|
|
49
|
+
* {
|
|
50
|
+
@apply border-border;
|
|
51
|
+
}
|
|
52
|
+
body {
|
|
53
|
+
@apply bg-background text-foreground antialiased;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import "./globals.css";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "arcie",
|
|
6
|
+
description: "Chat with your arcie agent.",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<html lang="en" className="h-full">
|
|
12
|
+
<body className="h-full">{children}</body>
|
|
13
|
+
</html>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Zap } from "lucide-react";
|
|
5
|
+
import { InputBar } from "@/components/input-bar";
|
|
6
|
+
import { Message } from "@/components/message";
|
|
7
|
+
import { readArcieStream } from "@/lib/stream";
|
|
8
|
+
import type { UiMessage, UiToolCall } from "@/lib/types";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
|
|
11
|
+
function newId(prefix: string): string {
|
|
12
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Chat() {
|
|
16
|
+
const [messages, setMessages] = React.useState<UiMessage[]>([]);
|
|
17
|
+
const [streaming, setStreaming] = React.useState(false);
|
|
18
|
+
const abortRef = React.useRef<AbortController | undefined>(undefined);
|
|
19
|
+
const bottomRef = React.useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
|
23
|
+
}, [messages]);
|
|
24
|
+
|
|
25
|
+
const stop = () => {
|
|
26
|
+
abortRef.current?.abort();
|
|
27
|
+
setStreaming(false);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const send = async (text: string) => {
|
|
31
|
+
const userMessage: UiMessage = { id: newId("u"), role: "user", content: text };
|
|
32
|
+
const assistantId = newId("a");
|
|
33
|
+
const assistantMessage: UiMessage = {
|
|
34
|
+
id: assistantId,
|
|
35
|
+
role: "assistant",
|
|
36
|
+
content: "",
|
|
37
|
+
streaming: true,
|
|
38
|
+
toolCalls: [],
|
|
39
|
+
};
|
|
40
|
+
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
|
41
|
+
setStreaming(true);
|
|
42
|
+
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
abortRef.current = controller;
|
|
45
|
+
|
|
46
|
+
const updateAssistant = (patch: (prev: UiMessage) => UiMessage) => {
|
|
47
|
+
setMessages((prev) =>
|
|
48
|
+
prev.map((message) => (message.id === assistantId ? patch(message) : message)),
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch("/api/chat", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({ message: text }),
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorBody = await response.text();
|
|
61
|
+
updateAssistant((m) => ({
|
|
62
|
+
...m,
|
|
63
|
+
content: errorBody || `Server error (${response.status})`,
|
|
64
|
+
streaming: false,
|
|
65
|
+
errored: true,
|
|
66
|
+
}));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const toolIndex = new Map<string, number>();
|
|
71
|
+
|
|
72
|
+
for await (const event of readArcieStream(response)) {
|
|
73
|
+
switch (event.type) {
|
|
74
|
+
case "message.appended": {
|
|
75
|
+
const delta = (event.data as { delta?: string }).delta ?? "";
|
|
76
|
+
updateAssistant((m) => ({ ...m, content: `${m.content}${delta}` }));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "message.completed": {
|
|
80
|
+
const text = (event.data as { text?: string | null }).text;
|
|
81
|
+
updateAssistant((m) => ({
|
|
82
|
+
...m,
|
|
83
|
+
content: typeof text === "string" && text.length > 0 ? text : m.content,
|
|
84
|
+
}));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "reasoning.appended": {
|
|
88
|
+
const delta = (event.data as { delta?: string }).delta ?? "";
|
|
89
|
+
updateAssistant((m) => ({ ...m, reasoning: `${m.reasoning ?? ""}${delta}` }));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "tool.started": {
|
|
93
|
+
const data = event.data as { name: string; callId: string; input: unknown };
|
|
94
|
+
const call: UiToolCall = {
|
|
95
|
+
callId: data.callId,
|
|
96
|
+
name: data.name,
|
|
97
|
+
input: data.input,
|
|
98
|
+
status: "running",
|
|
99
|
+
};
|
|
100
|
+
updateAssistant((m) => {
|
|
101
|
+
const toolCalls = m.toolCalls ?? [];
|
|
102
|
+
toolIndex.set(data.callId, toolCalls.length);
|
|
103
|
+
return { ...m, toolCalls: [...toolCalls, call] };
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "tool.completed": {
|
|
108
|
+
const data = event.data as {
|
|
109
|
+
callId: string;
|
|
110
|
+
output: unknown;
|
|
111
|
+
status: string;
|
|
112
|
+
error?: { code: string; message: string };
|
|
113
|
+
};
|
|
114
|
+
updateAssistant((m) => {
|
|
115
|
+
const toolCalls = [...(m.toolCalls ?? [])];
|
|
116
|
+
const idx = toolIndex.get(data.callId);
|
|
117
|
+
if (idx === undefined || toolCalls[idx] === undefined) return m;
|
|
118
|
+
const previous = toolCalls[idx];
|
|
119
|
+
const isApproval =
|
|
120
|
+
data.status === "pending" && data.error?.code === "needs_approval";
|
|
121
|
+
toolCalls[idx] = {
|
|
122
|
+
...previous,
|
|
123
|
+
status: isApproval
|
|
124
|
+
? "approval"
|
|
125
|
+
: data.status === "completed"
|
|
126
|
+
? "done"
|
|
127
|
+
: "error",
|
|
128
|
+
output: data.output,
|
|
129
|
+
errorMessage: data.error?.message,
|
|
130
|
+
};
|
|
131
|
+
return { ...m, toolCalls };
|
|
132
|
+
});
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "step.failed":
|
|
136
|
+
case "turn.failed":
|
|
137
|
+
case "session.failed": {
|
|
138
|
+
const data = event.data as { code?: string; message?: string };
|
|
139
|
+
updateAssistant((m) => ({
|
|
140
|
+
...m,
|
|
141
|
+
content: data.message ?? "Something went wrong.",
|
|
142
|
+
streaming: false,
|
|
143
|
+
errored: true,
|
|
144
|
+
}));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (controller.signal.aborted) {
|
|
151
|
+
updateAssistant((m) => ({ ...m, streaming: false }));
|
|
152
|
+
} else {
|
|
153
|
+
updateAssistant((m) => ({
|
|
154
|
+
...m,
|
|
155
|
+
content: error instanceof Error ? error.message : String(error),
|
|
156
|
+
streaming: false,
|
|
157
|
+
errored: true,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
updateAssistant((m) => ({ ...m, streaming: false }));
|
|
162
|
+
setStreaming(false);
|
|
163
|
+
abortRef.current = undefined;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="flex h-full flex-col">
|
|
169
|
+
<header className="flex items-center gap-2 border-b border-border px-4 py-3">
|
|
170
|
+
<Zap className="h-5 w-5 text-amber-500" />
|
|
171
|
+
<span className="font-semibold">arcie</span>
|
|
172
|
+
<span className="text-xs text-muted-foreground">web chat</span>
|
|
173
|
+
</header>
|
|
174
|
+
<div className="flex-1 overflow-y-auto">
|
|
175
|
+
<div className="mx-auto flex max-w-3xl flex-col gap-3 px-4 py-6">
|
|
176
|
+
{messages.length === 0 && <EmptyState />}
|
|
177
|
+
{messages.map((message) => (
|
|
178
|
+
<Message key={message.id} message={message} />
|
|
179
|
+
))}
|
|
180
|
+
<div ref={bottomRef} />
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<InputBar onSend={send} onStop={stop} streaming={streaming} />
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function EmptyState() {
|
|
189
|
+
return (
|
|
190
|
+
<div className={cn("mt-24 flex flex-col items-center gap-2 text-center text-muted-foreground")}>
|
|
191
|
+
<Zap className="h-8 w-8 text-amber-500" />
|
|
192
|
+
<div className="text-lg font-medium text-foreground">Start a conversation</div>
|
|
193
|
+
<div className="text-sm">Ask your arcie agent anything.</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Send, StopCircle } from "lucide-react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
interface InputBarProps {
|
|
9
|
+
onSend(message: string): void;
|
|
10
|
+
onStop(): void;
|
|
11
|
+
streaming: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function InputBar({ onSend, onStop, streaming, disabled }: InputBarProps) {
|
|
16
|
+
const [value, setValue] = React.useState("");
|
|
17
|
+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
18
|
+
|
|
19
|
+
const submit = () => {
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (trimmed.length === 0 || streaming || disabled) return;
|
|
22
|
+
onSend(trimmed);
|
|
23
|
+
setValue("");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
27
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
submit();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const el = textareaRef.current;
|
|
35
|
+
if (el === null) return;
|
|
36
|
+
el.style.height = "auto";
|
|
37
|
+
el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
|
|
38
|
+
}, [value]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="border-t border-border bg-background px-4 py-3">
|
|
42
|
+
<div className="mx-auto flex max-w-3xl items-end gap-2">
|
|
43
|
+
<textarea
|
|
44
|
+
ref={textareaRef}
|
|
45
|
+
value={value}
|
|
46
|
+
onChange={(event) => setValue(event.target.value)}
|
|
47
|
+
onKeyDown={onKeyDown}
|
|
48
|
+
rows={1}
|
|
49
|
+
placeholder={streaming ? "…" : "Send a message"}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground",
|
|
53
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
54
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
55
|
+
)}
|
|
56
|
+
/>
|
|
57
|
+
{streaming ? (
|
|
58
|
+
<Button size="icon" variant="outline" onClick={onStop} aria-label="Stop">
|
|
59
|
+
<StopCircle className="h-4 w-4" />
|
|
60
|
+
</Button>
|
|
61
|
+
) : (
|
|
62
|
+
<Button
|
|
63
|
+
size="icon"
|
|
64
|
+
onClick={submit}
|
|
65
|
+
disabled={value.trim().length === 0 || disabled}
|
|
66
|
+
aria-label="Send"
|
|
67
|
+
>
|
|
68
|
+
<Send className="h-4 w-4" />
|
|
69
|
+
</Button>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|