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.
Files changed (156) hide show
  1. package/README.md +34 -6
  2. package/dist/agent/index.d.ts +6 -3
  3. package/dist/agent/index.js +6 -6
  4. package/dist/agent/index.js.map +1 -1
  5. package/dist/auth/index.d.ts +6 -5
  6. package/dist/auth/index.js +8 -17
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/build-TWNCPXBU.js +82 -0
  9. package/dist/build-TWNCPXBU.js.map +1 -0
  10. package/dist/channels/index.d.ts +8 -5
  11. package/dist/channels/index.js +10 -12
  12. package/dist/channels/index.js.map +1 -1
  13. package/dist/chunk-3CNQ6HQV.js +12 -0
  14. package/dist/chunk-3CNQ6HQV.js.map +1 -0
  15. package/dist/chunk-4L7WVCTU.js +12 -0
  16. package/dist/chunk-4L7WVCTU.js.map +1 -0
  17. package/dist/chunk-4WSILP75.js +1774 -0
  18. package/dist/chunk-4WSILP75.js.map +1 -0
  19. package/dist/chunk-6XETPLIF.js +176 -0
  20. package/dist/chunk-6XETPLIF.js.map +1 -0
  21. package/dist/chunk-CKHBEZCT.js +24 -0
  22. package/dist/chunk-CKHBEZCT.js.map +1 -0
  23. package/dist/chunk-FQ7KDNV5.js +12 -0
  24. package/dist/chunk-FQ7KDNV5.js.map +1 -0
  25. package/dist/chunk-HVBDOHTL.js +148 -0
  26. package/dist/chunk-HVBDOHTL.js.map +1 -0
  27. package/dist/chunk-HWD2Z2CH.js +742 -0
  28. package/dist/chunk-HWD2Z2CH.js.map +1 -0
  29. package/dist/chunk-KVSX4MLK.js +139 -0
  30. package/dist/chunk-KVSX4MLK.js.map +1 -0
  31. package/dist/chunk-L3BTL5FM.js +39 -0
  32. package/dist/chunk-L3BTL5FM.js.map +1 -0
  33. package/dist/chunk-LQC65UHE.js +34 -0
  34. package/dist/chunk-LQC65UHE.js.map +1 -0
  35. package/dist/chunk-N5R3TEEZ.js +15 -0
  36. package/dist/chunk-N5R3TEEZ.js.map +1 -0
  37. package/dist/chunk-PHTJLJOV.js +25 -0
  38. package/dist/chunk-PHTJLJOV.js.map +1 -0
  39. package/dist/chunk-QE46QFID.js +92 -0
  40. package/dist/chunk-QE46QFID.js.map +1 -0
  41. package/dist/chunk-SGWQZZ2A.js +54 -0
  42. package/dist/chunk-SGWQZZ2A.js.map +1 -0
  43. package/dist/chunk-WAD7VLNI.js +51 -0
  44. package/dist/chunk-WAD7VLNI.js.map +1 -0
  45. package/dist/chunk-XOFWQUOO.js +30 -0
  46. package/dist/chunk-XOFWQUOO.js.map +1 -0
  47. package/dist/chunk-ZJNHQXMO.js +20 -0
  48. package/dist/chunk-ZJNHQXMO.js.map +1 -0
  49. package/dist/cli/index.d.ts +7 -2
  50. package/dist/cli/index.js +171 -41
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/context/index.d.ts +14 -11
  53. package/dist/context/index.js +22 -37
  54. package/dist/context/index.js.map +1 -1
  55. package/dist/dev-ZZDVOWUZ.js +1967 -0
  56. package/dist/dev-ZZDVOWUZ.js.map +1 -0
  57. package/dist/discover/index.d.ts +17 -7
  58. package/dist/discover/index.js +6 -132
  59. package/dist/discover/index.js.map +1 -1
  60. package/dist/hooks/index.d.ts +6 -3
  61. package/dist/hooks/index.js +6 -6
  62. package/dist/hooks/index.js.map +1 -1
  63. package/dist/index-CElLRVTP.d.ts +136 -0
  64. package/dist/index.d.ts +297 -18
  65. package/dist/index.js +160 -13
  66. package/dist/index.js.map +1 -1
  67. package/dist/init-5F7CIVLN.js +822 -0
  68. package/dist/init-5F7CIVLN.js.map +1 -0
  69. package/dist/instructions/index.d.ts +7 -4
  70. package/dist/instructions/index.js +8 -32
  71. package/dist/instructions/index.js.map +1 -1
  72. package/dist/protocol/events.d.ts +47 -39
  73. package/dist/protocol/events.js +46 -45
  74. package/dist/protocol/events.js.map +1 -1
  75. package/dist/runner/index.d.ts +4 -18
  76. package/dist/runner/index.js +11 -277
  77. package/dist/runner/index.js.map +1 -1
  78. package/dist/scaffold-web-chat-OQL5C2GK.js +7 -0
  79. package/dist/scaffold-web-chat-OQL5C2GK.js.map +1 -0
  80. package/dist/schedules/index.d.ts +6 -3
  81. package/dist/schedules/index.js +6 -6
  82. package/dist/schedules/index.js.map +1 -1
  83. package/dist/skills/index.d.ts +7 -4
  84. package/dist/skills/index.js +8 -27
  85. package/dist/skills/index.js.map +1 -1
  86. package/dist/tools/index.d.ts +13 -3
  87. package/dist/tools/index.js +8 -9
  88. package/dist/tools/index.js.map +1 -1
  89. package/dist/types-DwxwdSa2.d.ts +151 -0
  90. package/docs/getting-started.md +2 -2
  91. package/docs/index.md +3 -2
  92. package/docs/project-layout.md +2 -2
  93. package/docs/subagents.md +95 -0
  94. package/package.json +15 -10
  95. package/templates/default/.env.local +12 -0
  96. package/templates/default/agent/agent.ts +2 -2
  97. package/templates/default/package.json +3 -3
  98. package/templates/web-chat/.env.local.example +4 -0
  99. package/templates/web-chat/README.md +51 -0
  100. package/templates/web-chat/app/api/chat/route.ts +43 -0
  101. package/templates/web-chat/app/globals.css +55 -0
  102. package/templates/web-chat/app/layout.tsx +15 -0
  103. package/templates/web-chat/app/page.tsx +9 -0
  104. package/templates/web-chat/components/chat.tsx +196 -0
  105. package/templates/web-chat/components/input-bar.tsx +74 -0
  106. package/templates/web-chat/components/message.tsx +63 -0
  107. package/templates/web-chat/components/tool-call.tsx +79 -0
  108. package/templates/web-chat/components/ui/button.tsx +46 -0
  109. package/templates/web-chat/components/ui/input.tsx +19 -0
  110. package/templates/web-chat/components.json +20 -0
  111. package/templates/web-chat/lib/stream.ts +41 -0
  112. package/templates/web-chat/lib/types.ts +61 -0
  113. package/templates/web-chat/lib/utils.ts +6 -0
  114. package/templates/web-chat/next.config.mjs +4 -0
  115. package/templates/web-chat/package.json +32 -0
  116. package/templates/web-chat/postcss.config.mjs +6 -0
  117. package/templates/web-chat/tailwind.config.ts +59 -0
  118. package/templates/web-chat/tsconfig.json +23 -0
  119. package/dist/agent/index.d.ts.map +0 -1
  120. package/dist/auth/index.d.ts.map +0 -1
  121. package/dist/channels/index.d.ts.map +0 -1
  122. package/dist/cli/banner.d.ts +0 -2
  123. package/dist/cli/banner.d.ts.map +0 -1
  124. package/dist/cli/banner.js +0 -10
  125. package/dist/cli/banner.js.map +0 -1
  126. package/dist/cli/build.d.ts +0 -5
  127. package/dist/cli/build.d.ts.map +0 -1
  128. package/dist/cli/build.js +0 -54
  129. package/dist/cli/build.js.map +0 -1
  130. package/dist/cli/dev.d.ts +0 -5
  131. package/dist/cli/dev.d.ts.map +0 -1
  132. package/dist/cli/dev.js +0 -74
  133. package/dist/cli/dev.js.map +0 -1
  134. package/dist/cli/index.d.ts.map +0 -1
  135. package/dist/cli/init.d.ts +0 -4
  136. package/dist/cli/init.d.ts.map +0 -1
  137. package/dist/cli/init.js +0 -75
  138. package/dist/cli/init.js.map +0 -1
  139. package/dist/context/index.d.ts.map +0 -1
  140. package/dist/discover/index.d.ts.map +0 -1
  141. package/dist/hooks/index.d.ts.map +0 -1
  142. package/dist/index.d.ts.map +0 -1
  143. package/dist/instructions/index.d.ts.map +0 -1
  144. package/dist/loader.d.ts +0 -7
  145. package/dist/loader.d.ts.map +0 -1
  146. package/dist/loader.js +0 -102
  147. package/dist/loader.js.map +0 -1
  148. package/dist/protocol/events.d.ts.map +0 -1
  149. package/dist/runner/index.d.ts.map +0 -1
  150. package/dist/schedules/index.d.ts.map +0 -1
  151. package/dist/skills/index.d.ts.map +0 -1
  152. package/dist/tools/index.d.ts.map +0 -1
  153. package/dist/types.d.ts +0 -118
  154. package/dist/types.d.ts.map +0 -1
  155. package/dist/types.js +0 -2
  156. 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.3",
4
+ "version": "0.1.5",
5
5
  "description": "The electronic line — build agents at the speed of light.",
6
6
  "bin": {
7
- "eline": "dist/cli/index.js"
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": "tsc -p tsconfig.build.json",
41
- "dev": "tsc --watch",
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
- "eline",
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/eline.git"
61
+ "url": "git+https://github.com/cencori/arcie.git"
59
62
  },
60
- "homepage": "https://cencori.com/eline",
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": "^20.0.0",
70
- "typescript": "^5.3.0",
71
- "vitest": "^2.0.0"
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 "eline";
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 Eline agent powered by Cencori.",
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": "eline dev",
8
- "build": "eline build"
7
+ "dev": "arcie dev",
8
+ "build": "arcie build"
9
9
  },
10
10
  "dependencies": {
11
- "eline": "^0.1.2",
11
+ "arcie": "^0.1.2",
12
12
  "zod": "^3.23.0"
13
13
  },
14
14
  "devDependencies": {
@@ -0,0 +1,4 @@
1
+ # URL of the arcie dev / production HTTP server. The Next.js server route
2
+ # proxies chat messages to this URL. Keep this server-side only (no NEXT_PUBLIC_
3
+ # prefix) so the browser never talks to arcie directly.
4
+ ARCIE_URL=http://localhost:3000
@@ -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,9 @@
1
+ import { Chat } from "@/components/chat";
2
+
3
+ export default function Page() {
4
+ return (
5
+ <main className="flex h-full flex-col">
6
+ <Chat />
7
+ </main>
8
+ );
9
+ }
@@ -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
+ }