@cognition-ai/cloudflare-agents 0.0.0 → 0.0.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,107 @@
1
- # @cognition-ai/cloudflare-agents
1
+ # Cloudflare Agents - Devin chat agents
2
2
 
3
- This version (`0.0.0`) is a placeholder. The package is in private beta and has not been
4
- publicly released, so it deliberately fails to install.
3
+ Puts a [Devin](https://devin.ai) cloud session behind a
4
+ [Cloudflare Agents](https://developers.cloudflare.com/agents/) `AIChatAgent`, so `useAgentChat`
5
+ drives Devin the way it drives a model. Each chat is a Durable Object that owns one Devin session:
6
+ the API key stays in the Worker, and the session keeps working in the cloud while the Durable Object
7
+ hibernates.
8
+
9
+ Built on [`@cognition-ai/ai-sdk`](https://www.npmjs.com/package/@cognition-ai/ai-sdk), which does the
10
+ event-to-UI-message mapping.
11
+
12
+ ## Setup
5
13
 
6
14
  ```sh
7
- npm i @cognition-ai/cloudflare-agents@beta
15
+ bun add @cognition-ai/cloudflare-agents@beta @cognition-ai/sdk@beta agents @cloudflare/ai-chat ai
16
+ wrangler secret put DEVIN_API_KEY
17
+ ```
18
+
19
+ Like `@cognition-ai/sdk`, the package is beta-only: releases are `0.0.1-beta.N` prereleases on the
20
+ `beta` dist-tag, so the `@beta` suffix is required until the public launch.
21
+
22
+ ## Server
23
+
24
+ ```ts
25
+ import { DevinChatAgent } from "@cognition-ai/cloudflare-agents";
26
+ import { routeAgentRequest } from "agents";
27
+
28
+ export class DevinChat extends DevinChatAgent<Env> {}
29
+
30
+ export default {
31
+ async fetch(request, env) {
32
+ return (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 });
33
+ },
34
+ } satisfies ExportedHandler<Env>;
35
+ ```
36
+
37
+ ```jsonc
38
+ // wrangler.jsonc
39
+ {
40
+ "compatibility_flags": ["nodejs_compat"],
41
+ "durable_objects": { "bindings": [{ "class_name": "DevinChat", "name": "DevinChat" }] },
42
+ "migrations": [{ "new_sqlite_classes": ["DevinChat"], "tag": "v1" }],
43
+ }
44
+ ```
45
+
46
+ `DevinChatAgent` reads `DEVIN_API_KEY`, and optionally `DEVIN_ORG_ID` and `DEVIN_BASE_URL`, from the
47
+ Worker's bindings. Override the hooks for anything else:
48
+
49
+ ```ts
50
+ import { CloudDevin } from "@cognition-ai/sdk";
51
+
52
+ export class DevinChat extends DevinChatAgent<Env> {
53
+ protected override connectDevin() {
54
+ return CloudDevin.connect({ apiKey: this.env.DEVIN_API_KEY, orgId: this.env.DEVIN_ORG_ID });
55
+ }
56
+
57
+ protected override sessionOptions() {
58
+ return { repos: ["acme/app"] };
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Client
64
+
65
+ ```tsx
66
+ import type { DevinChatMessage, DevinChatState } from "@cognition-ai/cloudflare-agents";
67
+ import { useAgentChat } from "@cloudflare/ai-chat/react";
68
+ import { useAgent } from "agents/react";
69
+ import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
70
+
71
+ const agent = useAgent<DevinChatState>({ agent: "DevinChat", name: chatId });
72
+ const chat = useAgentChat<unknown, DevinChatMessage>({
73
+ agent,
74
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
75
+ });
8
76
  ```
9
77
 
10
- `0.0.0` exists only to hold the `latest` dist-tag — the npm registry assigns `latest` to
11
- whatever version a package is first published with, even when publishing with
12
- `--tag beta`, so without it a bare `npm i @cognition-ai/cloudflare-agents` would silently install a
13
- beta.
78
+ `agent.state` holds the session id and URL, status, activity, plan and pull requests, and updates
79
+ live. Messages carry text, reasoning, `dynamic-tool` parts, and `data-devin-plan` /
80
+ `data-devin-pull-request` parts; see `@cognition-ai/ai-sdk` for the full mapping.
81
+
82
+ ## What the agent does
83
+
84
+ - **One turn per message.** The first message creates a Devin session; later ones attach to it,
85
+ including after hibernation, a deploy, or from another client. Messages sent mid-turn queue.
86
+ - **Stop.** `chat.stop()` cancels the Devin turn.
87
+ - **Permissions.** Devin's permission requests arrive as AI SDK tool approvals. Answer them with
88
+ `chat.addToolApprovalResponse({ id, approved })`, and the same turn resumes. Approving picks
89
+ allow-once; denying picks reject-once.
90
+ - **Start over.** `agent.stub.startOver()` detaches from the session and stops the turn, so the next
91
+ message starts a new session. Clear the transcript with `chat.clearHistory()`. The old session
92
+ keeps running in the cloud.
93
+ - **Continue an existing session.** `agent.stub.importSession(id)` rebuilds the transcript from the
94
+ session's replay and attaches future turns to it. It throws on a chat that already has a session
95
+ or messages. `parseSessionId(input)` accepts a session id or a Devin web app URL.
96
+ - **Recovery.** If the Durable Object restarts mid-turn, what already streamed is kept and the
97
+ prompt is not sent again: Devin already has it.
98
+ - **Late events.** Events from a turn the chat moved on from (stopped, started over) never touch the
99
+ new session's state.
100
+
101
+ Workers can't start processes, so this uses cloud sessions only; `LocalDevin` needs Node.js or Bun.
102
+
103
+ ## Building your own agent
104
+
105
+ For a different agent shape, `connectDevin(env)` opens a connection from the bindings, and
106
+ `reduceDevinChatState(state, event)` projects session events onto `DevinChatState`. Combine them with
107
+ `toUIMessageStream`, `toUIMessages` and `ToolApprovals` from `@cognition-ai/ai-sdk`.
package/dist/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cognition AI, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ import { CloudDevin, type CloudDevinOptions } from "@cognition-ai/sdk";
2
+ /** The Worker bindings {@link connectDevin} reads. */
3
+ export interface DevinEnv {
4
+ /** A Devin API key (`cog_…`). Store it with `wrangler secret put DEVIN_API_KEY`. */
5
+ DEVIN_API_KEY?: string;
6
+ /** The organization sessions belong to, for keys that can reach several. */
7
+ DEVIN_ORG_ID?: string;
8
+ /** The Devin API, e.g. a staging deployment. Defaults to `https://api.devin.ai`. */
9
+ DEVIN_BASE_URL?: string;
10
+ }
11
+ /**
12
+ * Connect to Devin with a Worker's bindings. Empty `DEVIN_ORG_ID` and
13
+ * `DEVIN_BASE_URL` vars count as unset.
14
+ *
15
+ * @throws {DevinError} when `DEVIN_API_KEY` is missing, or the key is rejected.
16
+ */
17
+ export declare function connectDevin(env: DevinEnv, options?: Omit<CloudDevinOptions, "apiKey">): Promise<CloudDevin>;
@@ -0,0 +1,25 @@
1
+ import { CloudDevin, DevinError } from "@cognition-ai/sdk";
2
+ /**
3
+ * Connect to Devin with a Worker's bindings. Empty `DEVIN_ORG_ID` and
4
+ * `DEVIN_BASE_URL` vars count as unset.
5
+ *
6
+ * @throws {DevinError} when `DEVIN_API_KEY` is missing, or the key is rejected.
7
+ */
8
+ export async function connectDevin(env, options = {}) {
9
+ const apiKey = nonEmpty(env.DEVIN_API_KEY);
10
+ if (apiKey === undefined) {
11
+ throw new DevinError("DEVIN_API_KEY is not set. Add it to .dev.vars (or .env) for local development, or run `wrangler secret put DEVIN_API_KEY`.");
12
+ }
13
+ const orgId = nonEmpty(env.DEVIN_ORG_ID);
14
+ const baseUrl = nonEmpty(env.DEVIN_BASE_URL);
15
+ return CloudDevin.connect({
16
+ ...(orgId !== undefined ? { orgId } : {}),
17
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
18
+ ...options,
19
+ apiKey,
20
+ });
21
+ }
22
+ function nonEmpty(value) {
23
+ return value === undefined || value === "" ? undefined : value;
24
+ }
25
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAA0B,MAAM,mBAAmB,CAAC;AAYnF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAa,EACb,UAA6C,EAAE;IAE/C,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,UAAU,CAClB,4HAA4H,CAC7H,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC7C,OAAO,UAAU,CAAC,OAAO,CAAC;QACxB,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,GAAG,OAAO;QACV,MAAM;KACP,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ,CAAC,KAAyB;IACzC,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AACjE,CAAC","sourcesContent":["import { CloudDevin, DevinError, type CloudDevinOptions } from \"@cognition-ai/sdk\";\n\n/** The Worker bindings {@link connectDevin} reads. */\nexport interface DevinEnv {\n /** A Devin API key (`cog_…`). Store it with `wrangler secret put DEVIN_API_KEY`. */\n DEVIN_API_KEY?: string;\n /** The organization sessions belong to, for keys that can reach several. */\n DEVIN_ORG_ID?: string;\n /** The Devin API, e.g. a staging deployment. Defaults to `https://api.devin.ai`. */\n DEVIN_BASE_URL?: string;\n}\n\n/**\n * Connect to Devin with a Worker's bindings. Empty `DEVIN_ORG_ID` and\n * `DEVIN_BASE_URL` vars count as unset.\n *\n * @throws {DevinError} when `DEVIN_API_KEY` is missing, or the key is rejected.\n */\nexport async function connectDevin(\n env: DevinEnv,\n options: Omit<CloudDevinOptions, \"apiKey\"> = {},\n): Promise<CloudDevin> {\n const apiKey = nonEmpty(env.DEVIN_API_KEY);\n if (apiKey === undefined) {\n throw new DevinError(\n \"DEVIN_API_KEY is not set. Add it to .dev.vars (or .env) for local development, or run `wrangler secret put DEVIN_API_KEY`.\",\n );\n }\n const orgId = nonEmpty(env.DEVIN_ORG_ID);\n const baseUrl = nonEmpty(env.DEVIN_BASE_URL);\n return CloudDevin.connect({\n ...(orgId !== undefined ? { orgId } : {}),\n ...(baseUrl !== undefined ? { baseUrl } : {}),\n ...options,\n apiKey,\n });\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n return value === undefined || value === \"\" ? undefined : value;\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import { AIChatAgent, type ChatRecoveryContext, type ChatRecoveryOptions, type OnChatMessageOptions } from "@cloudflare/ai-chat";
2
+ import { type DevinMessageMetadata, type DevinUIMessage } from "@cognition-ai/ai-sdk";
3
+ import { type CloudDevin, type CloudSessionOptions } from "@cognition-ai/sdk";
4
+ import { type DevinEnv } from "./connect.js";
5
+ import { type DevinChatState } from "./state.js";
6
+ /** The messages a {@link DevinChatAgent} streams: `useAgentChat<DevinChatMessage>()`. */
7
+ export type DevinChatMessage = DevinUIMessage<DevinMessageMetadata>;
8
+ /** What {@link DevinChatAgent.importSession} rebuilt. */
9
+ export interface ImportedSession {
10
+ sessionId: string;
11
+ sessionUrl: string | null;
12
+ /** The first user message's text, e.g. for a chat title. */
13
+ title: string | null;
14
+ /** How many messages the replay produced. */
15
+ messages: number;
16
+ }
17
+ /**
18
+ * An `AIChatAgent` backed by a Devin cloud session instead of a model, so
19
+ * `useAgentChat` drives Devin unchanged.
20
+ *
21
+ * - Each chat message prompts the chat's Devin session and streams the turn
22
+ * with `@cognition-ai/ai-sdk`'s `toUIMessageStream`: text, reasoning, tool
23
+ * calls, plans and pull requests.
24
+ * - The session id lives in Agent state, so later turns attach to the same
25
+ * session after hibernation, a deploy or from another client. Devin keeps
26
+ * working in the cloud between turns.
27
+ * - Permission requests are AI SDK tool approvals. `addToolApprovalResponse`
28
+ * answers them and Cloudflare's auto-continuation resumes the turn.
29
+ * - Stopping the client cancels the Devin turn; {@link startOver} detaches.
30
+ *
31
+ * ```ts
32
+ * export class DevinChat extends DevinChatAgent<Env> {}
33
+ * ```
34
+ *
35
+ * reads `DEVIN_API_KEY` (and optional `DEVIN_ORG_ID`, `DEVIN_BASE_URL`) from
36
+ * the Worker's bindings. Override {@link connectDevin} for per-user tokens.
37
+ */
38
+ export declare class DevinChatAgent<Env extends Cloudflare.Env & DevinEnv = Cloudflare.Env & DevinEnv> extends AIChatAgent<Env, DevinChatState> {
39
+ #private;
40
+ initialState: DevinChatState;
41
+ /** A Devin session runs one turn at a time, so messages sent mid-turn wait their turn. */
42
+ messageConcurrency: "queue";
43
+ /** Open the Devin connection a turn uses. Override for per-user tokens or other options. */
44
+ protected connectDevin(): Promise<CloudDevin>;
45
+ /** Options for the session the first turn creates, e.g. `{ repos: ["acme/app"] }`. */
46
+ protected sessionOptions(): Omit<CloudSessionOptions, "onPermission">;
47
+ onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions): Promise<Response>;
48
+ /**
49
+ * Detach from the Devin session so the next message starts a new one, and
50
+ * stop the running turn. The old session keeps running in the cloud. Clear
51
+ * the transcript on the client with `clearHistory()`.
52
+ */
53
+ startOver(): void;
54
+ /**
55
+ * Start this chat from an existing cloud session: rebuild its transcript
56
+ * from the replayed tail and attach future turns to it.
57
+ *
58
+ * @throws {DevinError} when the chat already has a session or messages, or
59
+ * the session can't be loaded.
60
+ */
61
+ importSession(sessionId: string): Promise<ImportedSession>;
62
+ /**
63
+ * A Durable Object restart interrupted a turn. Keep what streamed, but don't
64
+ * send the prompt again: Devin already has it and keeps working in the cloud.
65
+ */
66
+ protected onChatRecovery(_ctx: ChatRecoveryContext): Promise<ChatRecoveryOptions>;
67
+ }
@@ -0,0 +1,404 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
36
+ if (value !== null && value !== void 0) {
37
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
38
+ var dispose, inner;
39
+ if (async) {
40
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
41
+ dispose = value[Symbol.asyncDispose];
42
+ }
43
+ if (dispose === void 0) {
44
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
45
+ dispose = value[Symbol.dispose];
46
+ if (async) inner = dispose;
47
+ }
48
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
49
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
50
+ env.stack.push({ value: value, dispose: dispose, async: async });
51
+ }
52
+ else if (async) {
53
+ env.stack.push({ async: true });
54
+ }
55
+ return value;
56
+ };
57
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
58
+ return function (env) {
59
+ function fail(e) {
60
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
61
+ env.hasError = true;
62
+ }
63
+ var r, s = 0;
64
+ function next() {
65
+ while (r = env.stack.pop()) {
66
+ try {
67
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
68
+ if (r.dispose) {
69
+ var result = r.dispose.call(r.value);
70
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
71
+ }
72
+ else s |= 1;
73
+ }
74
+ catch (e) {
75
+ fail(e);
76
+ }
77
+ }
78
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
79
+ if (env.hasError) throw env.error;
80
+ }
81
+ return next();
82
+ };
83
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
84
+ var e = new Error(message);
85
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
86
+ });
87
+ import { AIChatAgent, } from "@cloudflare/ai-chat";
88
+ import { devinSessionMetadata, toPromptInput, toUIMessages, toUIMessageStream, ToolApprovals, } from "@cognition-ai/ai-sdk";
89
+ import { DevinError, SessionEventReducer, } from "@cognition-ai/sdk";
90
+ import { callable } from "agents";
91
+ import { createUIMessageStream, createUIMessageStreamResponse, isToolUIPart, } from "ai";
92
+ import { connectDevin as connectFromEnv } from "./connect.js";
93
+ import { initialDevinChatState, reduceDevinChatState } from "./state.js";
94
+ /**
95
+ * An `AIChatAgent` backed by a Devin cloud session instead of a model, so
96
+ * `useAgentChat` drives Devin unchanged.
97
+ *
98
+ * - Each chat message prompts the chat's Devin session and streams the turn
99
+ * with `@cognition-ai/ai-sdk`'s `toUIMessageStream`: text, reasoning, tool
100
+ * calls, plans and pull requests.
101
+ * - The session id lives in Agent state, so later turns attach to the same
102
+ * session after hibernation, a deploy or from another client. Devin keeps
103
+ * working in the cloud between turns.
104
+ * - Permission requests are AI SDK tool approvals. `addToolApprovalResponse`
105
+ * answers them and Cloudflare's auto-continuation resumes the turn.
106
+ * - Stopping the client cancels the Devin turn; {@link startOver} detaches.
107
+ *
108
+ * ```ts
109
+ * export class DevinChat extends DevinChatAgent<Env> {}
110
+ * ```
111
+ *
112
+ * reads `DEVIN_API_KEY` (and optional `DEVIN_ORG_ID`, `DEVIN_BASE_URL`) from
113
+ * the Worker's bindings. Override {@link connectDevin} for per-user tokens.
114
+ */
115
+ let DevinChatAgent = (() => {
116
+ let _classSuper = AIChatAgent;
117
+ let _instanceExtraInitializers = [];
118
+ let _startOver_decorators;
119
+ return class DevinChatAgent extends _classSuper {
120
+ static {
121
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
122
+ _startOver_decorators = [callable()];
123
+ __esDecorate(this, null, _startOver_decorators, { kind: "method", name: "startOver", static: false, private: false, access: { has: obj => "startOver" in obj, get: obj => obj.startOver }, metadata: _metadata }, null, _instanceExtraInitializers);
124
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
125
+ }
126
+ initialState = (__runInitializers(this, _instanceExtraInitializers), initialDevinChatState);
127
+ /** A Devin session runs one turn at a time, so messages sent mid-turn wait their turn. */
128
+ messageConcurrency = "queue";
129
+ #turn;
130
+ /** Bumped whenever the running turn is replaced, so its late events can't touch state. */
131
+ #epoch = 0;
132
+ /** Open the Devin connection a turn uses. Override for per-user tokens or other options. */
133
+ connectDevin() {
134
+ return connectFromEnv(this.env);
135
+ }
136
+ /** Options for the session the first turn creates, e.g. `{ repos: ["acme/app"] }`. */
137
+ sessionOptions() {
138
+ return {};
139
+ }
140
+ onChatMessage(_onFinish, options) {
141
+ const signal = options?.abortSignal;
142
+ const turn = this.#turn;
143
+ const resumed = turn?.approvals.respond(this.messages);
144
+ if (turn !== undefined && resumed !== undefined) {
145
+ turn.follow(signal);
146
+ return Promise.resolve(createUIMessageStreamResponse({ stream: resumed }));
147
+ }
148
+ if (options?.continuation === true) {
149
+ return Promise.resolve(createUIMessageStreamResponse({ stream: this.#settleOrphans() }));
150
+ }
151
+ this.#replaceTurn();
152
+ return Promise.resolve(createUIMessageStreamResponse({ stream: this.#prompt(signal) }));
153
+ }
154
+ /**
155
+ * Detach from the Devin session so the next message starts a new one, and
156
+ * stop the running turn. The old session keeps running in the cloud. Clear
157
+ * the transcript on the client with `clearHistory()`.
158
+ */
159
+ startOver() {
160
+ this.#replaceTurn();
161
+ this.resetTurnState();
162
+ this.setState(initialDevinChatState);
163
+ }
164
+ /**
165
+ * Start this chat from an existing cloud session: rebuild its transcript
166
+ * from the replayed tail and attach future turns to it.
167
+ *
168
+ * @throws {DevinError} when the chat already has a session or messages, or
169
+ * the session can't be loaded.
170
+ */
171
+ async importSession(sessionId) {
172
+ if (this.state.sessionId !== null || this.messages.length > 0) {
173
+ throw new DevinError("This chat already has a Devin session.");
174
+ }
175
+ const loaded = await this.#load(sessionId);
176
+ const messages = await toUIMessages(loaded.replay);
177
+ const reducer = new SessionEventReducer();
178
+ let state = {
179
+ ...initialDevinChatState,
180
+ sessionId: loaded.sessionId,
181
+ sessionUrl: loaded.sessionUrl,
182
+ };
183
+ for (const update of loaded.replay)
184
+ state = reduceDevinChatState(state, reducer.handle(update));
185
+ await this.persistMessages(messages);
186
+ this.setState(state);
187
+ const first = messages.find((message) => message.role === "user");
188
+ const title = first?.parts
189
+ .flatMap((part) => (part.type === "text" ? [part.text] : []))
190
+ .join(" ");
191
+ return {
192
+ sessionId: loaded.sessionId,
193
+ sessionUrl: loaded.sessionUrl,
194
+ title: title === undefined || title === "" ? null : title,
195
+ messages: messages.length,
196
+ };
197
+ }
198
+ /**
199
+ * A Durable Object restart interrupted a turn. Keep what streamed, but don't
200
+ * send the prompt again: Devin already has it and keeps working in the cloud.
201
+ */
202
+ onChatRecovery(_ctx) {
203
+ return Promise.resolve({ persist: true, continue: false });
204
+ }
205
+ #prompt(signal) {
206
+ const epoch = this.#epoch;
207
+ return createUIMessageStream({
208
+ onError: errorText,
209
+ execute: async ({ writer }) => {
210
+ const prompt = toPromptInput(this.messages);
211
+ const devin = await this.connectDevin();
212
+ let session;
213
+ try {
214
+ session = await this.#openSession(devin, epoch);
215
+ }
216
+ catch (error) {
217
+ devin.close();
218
+ throw error;
219
+ }
220
+ if (signal?.aborted === true || epoch !== this.#epoch) {
221
+ devin.close();
222
+ return;
223
+ }
224
+ const turn = new Turn(devin, session, () => {
225
+ if (this.#turn === turn)
226
+ this.#turn = undefined;
227
+ });
228
+ this.#turn = turn;
229
+ turn.follow(signal);
230
+ writer.merge(toUIMessageStream(turn.events(prompt, toolCallIds(this.messages)), {
231
+ approvals: turn.approvals,
232
+ messageMetadata: devinSessionMetadata(session),
233
+ onError: errorText,
234
+ onEvent: (event) => {
235
+ this.#project(epoch, session, event);
236
+ },
237
+ }));
238
+ },
239
+ });
240
+ }
241
+ async #openSession(devin, epoch) {
242
+ const { sessionId } = this.state;
243
+ if (sessionId !== null)
244
+ return devin.attach(sessionId);
245
+ const session = await devin.createSession(this.sessionOptions());
246
+ if (epoch === this.#epoch) {
247
+ this.setState({ ...this.state, sessionId: session.id, sessionUrl: session.url ?? null });
248
+ }
249
+ return session;
250
+ }
251
+ #project(epoch, session, event) {
252
+ if (epoch !== this.#epoch || this.state.sessionId !== session.id)
253
+ return;
254
+ let next = reduceDevinChatState(this.state, event);
255
+ const url = session.url ?? next.sessionUrl;
256
+ if (url !== next.sessionUrl)
257
+ next = { ...next, sessionUrl: url };
258
+ if (next !== this.state)
259
+ this.setState(next);
260
+ }
261
+ /** Stop the running turn and fence off its late events. */
262
+ #replaceTurn() {
263
+ this.#epoch++;
264
+ const turn = this.#turn;
265
+ this.#turn = undefined;
266
+ turn?.stop();
267
+ }
268
+ /**
269
+ * An approval arrived for a turn this agent no longer holds (it was stopped,
270
+ * or the Durable Object restarted). End those tool calls instead of leaving
271
+ * them spinning.
272
+ */
273
+ #settleOrphans() {
274
+ const last = this.messages.at(-1);
275
+ const parts = last?.role === "assistant" ? last.parts.filter(isToolUIPart) : [];
276
+ return createUIMessageStream({
277
+ execute: ({ writer }) => {
278
+ writer.write({ type: "start" });
279
+ for (const part of parts) {
280
+ if (part.state !== "approval-responded")
281
+ continue;
282
+ writer.write({
283
+ type: "tool-output-error",
284
+ toolCallId: part.toolCallId,
285
+ errorText: "This Devin turn ended before the approval arrived.",
286
+ dynamic: true,
287
+ });
288
+ }
289
+ writer.write({ type: "finish", finishReason: "other" });
290
+ },
291
+ });
292
+ }
293
+ async #load(sessionId) {
294
+ const env_1 = { stack: [], error: void 0, hasError: false };
295
+ try {
296
+ const devin = __addDisposableResource(env_1, await this.connectDevin(), true);
297
+ try {
298
+ const { session, replay } = await devin.loadSession(sessionId, { replay: "tail" });
299
+ return { sessionId: session.id, sessionUrl: session.url ?? null, replay };
300
+ }
301
+ catch (error) {
302
+ throw new DevinError(`Could not load Devin session ${sessionId}: ${errorText(error)}`);
303
+ }
304
+ }
305
+ catch (e_1) {
306
+ env_1.error = e_1;
307
+ env_1.hasError = true;
308
+ }
309
+ finally {
310
+ const result_1 = __disposeResources(env_1);
311
+ if (result_1)
312
+ await result_1;
313
+ }
314
+ }
315
+ };
316
+ })();
317
+ export { DevinChatAgent };
318
+ /** One Devin turn: its connection, its approvals, and a way to stop it. */
319
+ class Turn {
320
+ approvals = new ToolApprovals();
321
+ #devin;
322
+ #session;
323
+ #stop = new AbortController();
324
+ #onClose;
325
+ #started = false;
326
+ #closed = false;
327
+ constructor(devin, session, onClose) {
328
+ this.#devin = devin;
329
+ this.#session = session;
330
+ this.#onClose = onClose;
331
+ session.onPermission = this.approvals.onPermission;
332
+ }
333
+ /**
334
+ * The turn's events, minus updates for tool calls that belong to an earlier
335
+ * message: stopping a turn does not stop the command Devin was running, so
336
+ * it can finish during a later turn. Its part lives in a message this
337
+ * stream can't reach, and `AIChatAgent` folds messages that share a tool
338
+ * call id into one, which would overwrite the earlier reply.
339
+ */
340
+ async *events(prompt, earlierToolCalls) {
341
+ this.#started = true;
342
+ const turn = this.#session.stream(prompt, { signal: this.#stop.signal });
343
+ let finished = false;
344
+ try {
345
+ for (;;) {
346
+ const next = await turn.next();
347
+ if (next.done) {
348
+ finished = true;
349
+ return next.value;
350
+ }
351
+ const event = next.value;
352
+ if (event.type === "tool_call" && earlierToolCalls.has(event.call.id))
353
+ continue;
354
+ yield event;
355
+ }
356
+ }
357
+ finally {
358
+ // The consumer stopped early: let the SDK's generator run its cleanup too.
359
+ if (!finished)
360
+ await turn.return(undefined);
361
+ this.#close();
362
+ }
363
+ }
364
+ /** Stop the turn when this request's signal aborts (the client pressed stop). */
365
+ follow(signal) {
366
+ if (signal === undefined)
367
+ return;
368
+ if (signal.aborted) {
369
+ this.stop();
370
+ return;
371
+ }
372
+ signal.addEventListener("abort", () => {
373
+ this.stop();
374
+ }, { once: true });
375
+ }
376
+ stop() {
377
+ this.#stop.abort(new DevinError("The Devin turn was stopped."));
378
+ this.approvals.cancel();
379
+ if (!this.#started)
380
+ this.#close();
381
+ }
382
+ #close() {
383
+ if (this.#closed)
384
+ return;
385
+ this.#closed = true;
386
+ this.approvals.cancel();
387
+ this.#devin.close();
388
+ this.#onClose();
389
+ }
390
+ }
391
+ /** Ids of every tool call in the transcript so far. */
392
+ function toolCallIds(messages) {
393
+ const ids = new Set();
394
+ for (const message of messages) {
395
+ for (const part of message.parts)
396
+ if (isToolUIPart(part))
397
+ ids.add(part.toolCallId);
398
+ }
399
+ return ids;
400
+ }
401
+ function errorText(error) {
402
+ return error instanceof Error ? error.message : String(error);
403
+ }
404
+ //# sourceMappingURL=devin-chat-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devin-chat-agent.js","sourceRoot":"","sources":["../src/devin-chat-agent.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AIChatAgent,\n type ChatRecoveryContext,\n type ChatRecoveryOptions,\n type OnChatMessageOptions,\n} from \"@cloudflare/ai-chat\";\nimport {\n devinSessionMetadata,\n toPromptInput,\n toUIMessages,\n toUIMessageStream,\n ToolApprovals,\n type DevinMessageMetadata,\n type DevinUIMessage,\n} from \"@cognition-ai/ai-sdk\";\nimport {\n DevinError,\n SessionEventReducer,\n type CloudDevin,\n type CloudSession,\n type CloudSessionOptions,\n type PromptInput,\n type PromptResult,\n type SessionEvent,\n} from \"@cognition-ai/sdk\";\nimport { callable } from \"agents\";\nimport {\n createUIMessageStream,\n createUIMessageStreamResponse,\n isToolUIPart,\n type InferUIMessageChunk,\n type UIMessage,\n} from \"ai\";\n\nimport { connectDevin as connectFromEnv, type DevinEnv } from \"./connect.js\";\nimport { initialDevinChatState, reduceDevinChatState, type DevinChatState } from \"./state.js\";\n\n/** The messages a {@link DevinChatAgent} streams: `useAgentChat<DevinChatMessage>()`. */\nexport type DevinChatMessage = DevinUIMessage<DevinMessageMetadata>;\n\ntype DevinChatChunk = InferUIMessageChunk<DevinChatMessage>;\n\n/** What {@link DevinChatAgent.importSession} rebuilt. */\nexport interface ImportedSession {\n sessionId: string;\n sessionUrl: string | null;\n /** The first user message's text, e.g. for a chat title. */\n title: string | null;\n /** How many messages the replay produced. */\n messages: number;\n}\n\n/**\n * An `AIChatAgent` backed by a Devin cloud session instead of a model, so\n * `useAgentChat` drives Devin unchanged.\n *\n * - Each chat message prompts the chat's Devin session and streams the turn\n * with `@cognition-ai/ai-sdk`'s `toUIMessageStream`: text, reasoning, tool\n * calls, plans and pull requests.\n * - The session id lives in Agent state, so later turns attach to the same\n * session after hibernation, a deploy or from another client. Devin keeps\n * working in the cloud between turns.\n * - Permission requests are AI SDK tool approvals. `addToolApprovalResponse`\n * answers them and Cloudflare's auto-continuation resumes the turn.\n * - Stopping the client cancels the Devin turn; {@link startOver} detaches.\n *\n * ```ts\n * export class DevinChat extends DevinChatAgent<Env> {}\n * ```\n *\n * reads `DEVIN_API_KEY` (and optional `DEVIN_ORG_ID`, `DEVIN_BASE_URL`) from\n * the Worker's bindings. Override {@link connectDevin} for per-user tokens.\n */\nexport class DevinChatAgent<\n Env extends Cloudflare.Env & DevinEnv = Cloudflare.Env & DevinEnv,\n> extends AIChatAgent<Env, DevinChatState> {\n override initialState: DevinChatState = initialDevinChatState;\n /** A Devin session runs one turn at a time, so messages sent mid-turn wait their turn. */\n override messageConcurrency = \"queue\" as const;\n\n #turn: Turn | undefined;\n /** Bumped whenever the running turn is replaced, so its late events can't touch state. */\n #epoch = 0;\n\n /** Open the Devin connection a turn uses. Override for per-user tokens or other options. */\n protected connectDevin(): Promise<CloudDevin> {\n return connectFromEnv(this.env);\n }\n\n /** Options for the session the first turn creates, e.g. `{ repos: [\"acme/app\"] }`. */\n protected sessionOptions(): Omit<CloudSessionOptions, \"onPermission\"> {\n return {};\n }\n\n override onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions): Promise<Response> {\n const signal = options?.abortSignal;\n const turn = this.#turn;\n const resumed = turn?.approvals.respond(this.messages);\n if (turn !== undefined && resumed !== undefined) {\n turn.follow(signal);\n return Promise.resolve(createUIMessageStreamResponse({ stream: resumed }));\n }\n if (options?.continuation === true) {\n return Promise.resolve(createUIMessageStreamResponse({ stream: this.#settleOrphans() }));\n }\n this.#replaceTurn();\n return Promise.resolve(createUIMessageStreamResponse({ stream: this.#prompt(signal) }));\n }\n\n /**\n * Detach from the Devin session so the next message starts a new one, and\n * stop the running turn. The old session keeps running in the cloud. Clear\n * the transcript on the client with `clearHistory()`.\n */\n @callable()\n startOver(): void {\n this.#replaceTurn();\n this.resetTurnState();\n this.setState(initialDevinChatState);\n }\n\n /**\n * Start this chat from an existing cloud session: rebuild its transcript\n * from the replayed tail and attach future turns to it.\n *\n * @throws {DevinError} when the chat already has a session or messages, or\n * the session can't be loaded.\n */\n async importSession(sessionId: string): Promise<ImportedSession> {\n if (this.state.sessionId !== null || this.messages.length > 0) {\n throw new DevinError(\"This chat already has a Devin session.\");\n }\n const loaded = await this.#load(sessionId);\n const messages = await toUIMessages(loaded.replay);\n const reducer = new SessionEventReducer();\n let state: DevinChatState = {\n ...initialDevinChatState,\n sessionId: loaded.sessionId,\n sessionUrl: loaded.sessionUrl,\n };\n for (const update of loaded.replay) state = reduceDevinChatState(state, reducer.handle(update));\n\n await this.persistMessages(messages);\n this.setState(state);\n const first = messages.find((message) => message.role === \"user\");\n const title = first?.parts\n .flatMap((part) => (part.type === \"text\" ? [part.text] : []))\n .join(\" \");\n return {\n sessionId: loaded.sessionId,\n sessionUrl: loaded.sessionUrl,\n title: title === undefined || title === \"\" ? null : title,\n messages: messages.length,\n };\n }\n\n /**\n * A Durable Object restart interrupted a turn. Keep what streamed, but don't\n * send the prompt again: Devin already has it and keeps working in the cloud.\n */\n protected override onChatRecovery(_ctx: ChatRecoveryContext): Promise<ChatRecoveryOptions> {\n return Promise.resolve({ persist: true, continue: false });\n }\n\n #prompt(signal: AbortSignal | undefined): ReadableStream<DevinChatChunk> {\n const epoch = this.#epoch;\n return createUIMessageStream<DevinChatMessage>({\n onError: errorText,\n execute: async ({ writer }) => {\n const prompt = toPromptInput(this.messages);\n const devin = await this.connectDevin();\n let session: CloudSession;\n try {\n session = await this.#openSession(devin, epoch);\n } catch (error) {\n devin.close();\n throw error;\n }\n if (signal?.aborted === true || epoch !== this.#epoch) {\n devin.close();\n return;\n }\n const turn = new Turn(devin, session, () => {\n if (this.#turn === turn) this.#turn = undefined;\n });\n this.#turn = turn;\n turn.follow(signal);\n writer.merge(\n toUIMessageStream(turn.events(prompt, toolCallIds(this.messages)), {\n approvals: turn.approvals,\n messageMetadata: devinSessionMetadata(session),\n onError: errorText,\n onEvent: (event) => {\n this.#project(epoch, session, event);\n },\n }),\n );\n },\n });\n }\n\n async #openSession(devin: CloudDevin, epoch: number): Promise<CloudSession> {\n const { sessionId } = this.state;\n if (sessionId !== null) return devin.attach(sessionId);\n const session = await devin.createSession(this.sessionOptions());\n if (epoch === this.#epoch) {\n this.setState({ ...this.state, sessionId: session.id, sessionUrl: session.url ?? null });\n }\n return session;\n }\n\n #project(epoch: number, session: CloudSession, event: SessionEvent): void {\n if (epoch !== this.#epoch || this.state.sessionId !== session.id) return;\n let next = reduceDevinChatState(this.state, event);\n const url = session.url ?? next.sessionUrl;\n if (url !== next.sessionUrl) next = { ...next, sessionUrl: url };\n if (next !== this.state) this.setState(next);\n }\n\n /** Stop the running turn and fence off its late events. */\n #replaceTurn(): void {\n this.#epoch++;\n const turn = this.#turn;\n this.#turn = undefined;\n turn?.stop();\n }\n\n /**\n * An approval arrived for a turn this agent no longer holds (it was stopped,\n * or the Durable Object restarted). End those tool calls instead of leaving\n * them spinning.\n */\n #settleOrphans(): ReadableStream<DevinChatChunk> {\n const last = this.messages.at(-1);\n const parts = last?.role === \"assistant\" ? last.parts.filter(isToolUIPart) : [];\n return createUIMessageStream<DevinChatMessage>({\n execute: ({ writer }) => {\n writer.write({ type: \"start\" });\n for (const part of parts) {\n if (part.state !== \"approval-responded\") continue;\n writer.write({\n type: \"tool-output-error\",\n toolCallId: part.toolCallId,\n errorText: \"This Devin turn ended before the approval arrived.\",\n dynamic: true,\n });\n }\n writer.write({ type: \"finish\", finishReason: \"other\" });\n },\n });\n }\n\n async #load(\n sessionId: string,\n ): Promise<{ sessionId: string; sessionUrl: string | null; replay: LoadedReplay }> {\n await using devin = await this.connectDevin();\n try {\n const { session, replay } = await devin.loadSession(sessionId, { replay: \"tail\" });\n return { sessionId: session.id, sessionUrl: session.url ?? null, replay };\n } catch (error) {\n throw new DevinError(`Could not load Devin session ${sessionId}: ${errorText(error)}`);\n }\n }\n}\n\ntype LoadedReplay = Awaited<ReturnType<CloudDevin[\"loadSession\"]>>[\"replay\"];\n\n/** One Devin turn: its connection, its approvals, and a way to stop it. */\nclass Turn {\n readonly approvals = new ToolApprovals();\n readonly #devin: CloudDevin;\n readonly #session: CloudSession;\n readonly #stop = new AbortController();\n readonly #onClose: () => void;\n #started = false;\n #closed = false;\n\n constructor(devin: CloudDevin, session: CloudSession, onClose: () => void) {\n this.#devin = devin;\n this.#session = session;\n this.#onClose = onClose;\n session.onPermission = this.approvals.onPermission;\n }\n\n /**\n * The turn's events, minus updates for tool calls that belong to an earlier\n * message: stopping a turn does not stop the command Devin was running, so\n * it can finish during a later turn. Its part lives in a message this\n * stream can't reach, and `AIChatAgent` folds messages that share a tool\n * call id into one, which would overwrite the earlier reply.\n */\n async *events(\n prompt: PromptInput,\n earlierToolCalls: ReadonlySet<string>,\n ): AsyncGenerator<SessionEvent, PromptResult> {\n this.#started = true;\n const turn = this.#session.stream(prompt, { signal: this.#stop.signal });\n let finished = false;\n try {\n for (;;) {\n const next = await turn.next();\n if (next.done) {\n finished = true;\n return next.value;\n }\n const event = next.value;\n if (event.type === \"tool_call\" && earlierToolCalls.has(event.call.id)) continue;\n yield event;\n }\n } finally {\n // The consumer stopped early: let the SDK's generator run its cleanup too.\n if (!finished) await turn.return(undefined as never);\n this.#close();\n }\n }\n\n /** Stop the turn when this request's signal aborts (the client pressed stop). */\n follow(signal: AbortSignal | undefined): void {\n if (signal === undefined) return;\n if (signal.aborted) {\n this.stop();\n return;\n }\n signal.addEventListener(\n \"abort\",\n () => {\n this.stop();\n },\n { once: true },\n );\n }\n\n stop(): void {\n this.#stop.abort(new DevinError(\"The Devin turn was stopped.\"));\n this.approvals.cancel();\n if (!this.#started) this.#close();\n }\n\n #close(): void {\n if (this.#closed) return;\n this.#closed = true;\n this.approvals.cancel();\n this.#devin.close();\n this.#onClose();\n }\n}\n\n/** Ids of every tool call in the transcript so far. */\nfunction toolCallIds(messages: readonly UIMessage[]): Set<string> {\n const ids = new Set<string>();\n for (const message of messages) {\n for (const part of message.parts) if (isToolUIPart(part)) ids.add(part.toolCallId);\n }\n return ids;\n}\n\nfunction errorText(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export { connectDevin, type DevinEnv } from "./connect.js";
2
+ export { DevinChatAgent, type DevinChatMessage, type ImportedSession } from "./devin-chat-agent.js";
3
+ export { parseSessionId } from "./session-id.js";
4
+ export { initialDevinChatState, reduceDevinChatState, type DevinChatState } from "./state.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { connectDevin } from "./connect.js";
2
+ export { DevinChatAgent } from "./devin-chat-agent.js";
3
+ export { parseSessionId } from "./session-id.js";
4
+ export { initialDevinChatState, reduceDevinChatState } from "./state.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAiB,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,cAAc,EAA+C,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAuB,MAAM,YAAY,CAAC","sourcesContent":["export { connectDevin, type DevinEnv } from \"./connect.js\";\nexport { DevinChatAgent, type DevinChatMessage, type ImportedSession } from \"./devin-chat-agent.js\";\nexport { parseSessionId } from \"./session-id.js\";\nexport { initialDevinChatState, reduceDevinChatState, type DevinChatState } from \"./state.js\";\n"]}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A session id from what a user pastes: an id (`devin-…`) or the session's
3
+ * web app URL (`https://app.devin.ai/sessions/…`).
4
+ *
5
+ * @throws {DevinError} when `input` is neither.
6
+ */
7
+ export declare function parseSessionId(input: unknown): string;
@@ -0,0 +1,25 @@
1
+ import { DevinError } from "@cognition-ai/sdk";
2
+ const SESSION_ID = /^[A-Za-z0-9_-]{1,128}$/;
3
+ /**
4
+ * A session id from what a user pastes: an id (`devin-…`) or the session's
5
+ * web app URL (`https://app.devin.ai/sessions/…`).
6
+ *
7
+ * @throws {DevinError} when `input` is neither.
8
+ */
9
+ export function parseSessionId(input) {
10
+ if (typeof input !== "string")
11
+ throw new DevinError("Expected a Devin session id or URL.");
12
+ const trimmed = input.trim();
13
+ let id = trimmed;
14
+ if (URL.canParse(trimmed)) {
15
+ const segments = new URL(trimmed).pathname.split("/").filter(Boolean);
16
+ const at = segments.indexOf("sessions");
17
+ id = at >= 0 ? (segments[at + 1] ?? "") : "";
18
+ if (id !== "" && !id.startsWith("devin-"))
19
+ id = `devin-${id}`;
20
+ }
21
+ if (!SESSION_ID.test(id))
22
+ throw new DevinError(`Not a Devin session id or URL: ${trimmed}`);
23
+ return id;
24
+ }
25
+ //# sourceMappingURL=session-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-id.js","sourceRoot":"","sources":["../src/session-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAC;IAC3F,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,EAAE,GAAG,OAAO,CAAC;IACjB,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACtE,MAAM,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACxC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,EAAE,GAAG,SAAS,EAAE,EAAE,CAAC;IAChE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,kCAAkC,OAAO,EAAE,CAAC,CAAC;IAC5F,OAAO,EAAE,CAAC;AACZ,CAAC","sourcesContent":["import { DevinError } from \"@cognition-ai/sdk\";\n\nconst SESSION_ID = /^[A-Za-z0-9_-]{1,128}$/;\n\n/**\n * A session id from what a user pastes: an id (`devin-…`) or the session's\n * web app URL (`https://app.devin.ai/sessions/…`).\n *\n * @throws {DevinError} when `input` is neither.\n */\nexport function parseSessionId(input: unknown): string {\n if (typeof input !== \"string\") throw new DevinError(\"Expected a Devin session id or URL.\");\n const trimmed = input.trim();\n let id = trimmed;\n if (URL.canParse(trimmed)) {\n const segments = new URL(trimmed).pathname.split(\"/\").filter(Boolean);\n const at = segments.indexOf(\"sessions\");\n id = at >= 0 ? (segments[at + 1] ?? \"\") : \"\";\n if (id !== \"\" && !id.startsWith(\"devin-\")) id = `devin-${id}`;\n }\n if (!SESSION_ID.test(id)) throw new DevinError(`Not a Devin session id or URL: ${trimmed}`);\n return id;\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { PlanEntry, SessionEvent, SessionStatus, TurnState } from "@cognition-ai/sdk";
2
+ /** What a {@link DevinChatAgent} knows about its Devin session, synced to clients as Agent state. */
3
+ export interface DevinChatState {
4
+ /** The attached Devin session (`devin-…`), or `null` until the first turn creates one. */
5
+ sessionId: string | null;
6
+ /** The session in the Devin web app. */
7
+ sessionUrl: string | null;
8
+ status: SessionStatus | null;
9
+ statusMessage: string | null;
10
+ /** What Devin is doing right now, e.g. `waiting_for_ci`. Cleared when it stops working. */
11
+ activity: string | null;
12
+ turnState: TurnState | null;
13
+ plan: PlanEntry[];
14
+ /** Pull requests the session opened, oldest first. */
15
+ pullRequests: string[];
16
+ }
17
+ export declare const initialDevinChatState: DevinChatState;
18
+ /**
19
+ * Fold one session event into {@link DevinChatState}. Returns `state` itself
20
+ * when nothing changed, so callers can skip `setState`. Transcript events
21
+ * (text, reasoning, tools) belong in messages, not state, and are ignored.
22
+ */
23
+ export declare function reduceDevinChatState(state: DevinChatState, event: SessionEvent): DevinChatState;
package/dist/state.js ADDED
@@ -0,0 +1,50 @@
1
+ export const initialDevinChatState = {
2
+ sessionId: null,
3
+ sessionUrl: null,
4
+ status: null,
5
+ statusMessage: null,
6
+ activity: null,
7
+ turnState: null,
8
+ plan: [],
9
+ pullRequests: [],
10
+ };
11
+ /**
12
+ * Fold one session event into {@link DevinChatState}. Returns `state` itself
13
+ * when nothing changed, so callers can skip `setState`. Transcript events
14
+ * (text, reasoning, tools) belong in messages, not state, and are ignored.
15
+ */
16
+ export function reduceDevinChatState(state, event) {
17
+ switch (event.type) {
18
+ case "status":
19
+ return {
20
+ ...state,
21
+ status: event.status,
22
+ statusMessage: event.message ?? null,
23
+ activity: event.status === "working" ? state.activity : null,
24
+ };
25
+ case "activity":
26
+ return event.activity === state.activity ? state : { ...state, activity: event.activity };
27
+ case "lifecycle":
28
+ return event.status === undefined
29
+ ? state
30
+ : { ...state, status: event.status, activity: null };
31
+ case "plan":
32
+ return { ...state, plan: event.entries };
33
+ case "pull_request":
34
+ return state.pullRequests.includes(event.prUrl)
35
+ ? state
36
+ : { ...state, pullRequests: [...state.pullRequests, event.prUrl] };
37
+ case "turn_state":
38
+ return event.state === state.turnState ? state : { ...state, turnState: event.state };
39
+ case "message_delta":
40
+ case "thought_delta":
41
+ case "tool_call":
42
+ case "user_message":
43
+ case "typing":
44
+ case "mode_change":
45
+ case "commands_change":
46
+ case "raw":
47
+ return state;
48
+ }
49
+ }
50
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAkBA,MAAM,CAAC,MAAM,qBAAqB,GAAmB;IACnD,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,IAAI;IAChB,MAAM,EAAE,IAAI;IACZ,aAAa,EAAE,IAAI;IACnB,QAAQ,EAAE,IAAI;IACd,SAAS,EAAE,IAAI;IACf,IAAI,EAAE,EAAE;IACR,YAAY,EAAE,EAAE;CACjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAqB,EAAE,KAAmB;IAC7E,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,QAAQ;YACX,OAAO;gBACL,GAAG,KAAK;gBACR,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,aAAa,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI;gBACpC,QAAQ,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;aAC7D,CAAC;QACJ,KAAK,UAAU;YACb,OAAO,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC5F,KAAK,WAAW;YACd,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS;gBAC/B,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACzD,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QAC3C,KAAK,cAAc;YACjB,OAAO,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;gBAC7C,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACvE,KAAK,YAAY;YACf,OAAO,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;QACxF,KAAK,eAAe,CAAC;QACrB,KAAK,eAAe,CAAC;QACrB,KAAK,WAAW,CAAC;QACjB,KAAK,cAAc,CAAC;QACpB,KAAK,QAAQ,CAAC;QACd,KAAK,aAAa,CAAC;QACnB,KAAK,iBAAiB,CAAC;QACvB,KAAK,KAAK;YACR,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC","sourcesContent":["import type { PlanEntry, SessionEvent, SessionStatus, TurnState } from \"@cognition-ai/sdk\";\n\n/** What a {@link DevinChatAgent} knows about its Devin session, synced to clients as Agent state. */\nexport interface DevinChatState {\n /** The attached Devin session (`devin-…`), or `null` until the first turn creates one. */\n sessionId: string | null;\n /** The session in the Devin web app. */\n sessionUrl: string | null;\n status: SessionStatus | null;\n statusMessage: string | null;\n /** What Devin is doing right now, e.g. `waiting_for_ci`. Cleared when it stops working. */\n activity: string | null;\n turnState: TurnState | null;\n plan: PlanEntry[];\n /** Pull requests the session opened, oldest first. */\n pullRequests: string[];\n}\n\nexport const initialDevinChatState: DevinChatState = {\n sessionId: null,\n sessionUrl: null,\n status: null,\n statusMessage: null,\n activity: null,\n turnState: null,\n plan: [],\n pullRequests: [],\n};\n\n/**\n * Fold one session event into {@link DevinChatState}. Returns `state` itself\n * when nothing changed, so callers can skip `setState`. Transcript events\n * (text, reasoning, tools) belong in messages, not state, and are ignored.\n */\nexport function reduceDevinChatState(state: DevinChatState, event: SessionEvent): DevinChatState {\n switch (event.type) {\n case \"status\":\n return {\n ...state,\n status: event.status,\n statusMessage: event.message ?? null,\n activity: event.status === \"working\" ? state.activity : null,\n };\n case \"activity\":\n return event.activity === state.activity ? state : { ...state, activity: event.activity };\n case \"lifecycle\":\n return event.status === undefined\n ? state\n : { ...state, status: event.status, activity: null };\n case \"plan\":\n return { ...state, plan: event.entries };\n case \"pull_request\":\n return state.pullRequests.includes(event.prUrl)\n ? state\n : { ...state, pullRequests: [...state.pullRequests, event.prUrl] };\n case \"turn_state\":\n return event.state === state.turnState ? state : { ...state, turnState: event.state };\n case \"message_delta\":\n case \"thought_delta\":\n case \"tool_call\":\n case \"user_message\":\n case \"typing\":\n case \"mode_change\":\n case \"commands_change\":\n case \"raw\":\n return state;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,19 +1,57 @@
1
1
  {
2
2
  "name": "@cognition-ai/cloudflare-agents",
3
- "version": "0.0.0",
4
- "description": "Not publicly released yet. Install the beta: npm i @cognition-ai/cloudflare-agents@beta",
3
+ "version": "0.0.1-beta.2",
4
+ "description": "Cloudflare Agents adapter for the Devin SDK: a Devin cloud session behind an AIChatAgent, for useAgentChat.",
5
5
  "license": "MIT",
6
- "os": [
7
- "beta-only-release--run--npm-i-@cognition-ai/cloudflare-agents@beta"
8
- ],
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
9
13
  "files": [
10
- "README.md"
14
+ "dist"
11
15
  ],
16
+ "sideEffects": false,
12
17
  "publishConfig": {
13
18
  "access": "public",
14
- "tag": "latest"
19
+ "tag": "beta"
20
+ },
21
+ "engines": {
22
+ "node": ">=22"
15
23
  },
16
24
  "scripts": {
17
- "preinstall": "node -e \"console.error('\\n@cognition-ai/cloudflare-agents is not publicly released yet.\\nInstall the beta instead:\\n\\n npm i @cognition-ai/cloudflare-agents@beta\\n'); process.exit(1)\""
18
- }
25
+ "build": "node ../../scripts/prepare-package.mjs && tsc -p tsconfig.build.json",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test --noEmit",
27
+ "test": "vitest run --config test/vitest.config.ts"
28
+ },
29
+ "dependencies": {
30
+ "@cognition-ai/ai-sdk": "0.0.1-beta.2",
31
+ "@cognition-ai/sdk": "0.0.1-beta.2"
32
+ },
33
+ "peerDependencies": {
34
+ "@cloudflare/ai-chat": ">=0.11.0 <1.0.0",
35
+ "agents": ">=0.22.0 <1.0.0",
36
+ "ai": "^7.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@babel/plugin-proposal-decorators": "8.0.2",
40
+ "@cloudflare/ai-chat": "0.11.0",
41
+ "@cloudflare/vitest-pool-workers": "0.22.0",
42
+ "@cloudflare/workers-types": "5.20260903.1",
43
+ "@types/node": "^22.10.0",
44
+ "@types/ws": "^8.5.13",
45
+ "agents": "0.22.0",
46
+ "ai": "7.0.93",
47
+ "typescript": "^5.8.3",
48
+ "vitest": "4.1.11",
49
+ "ws": "^8.21.0"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/COG-GTM/devin-sdk-examples.git"
54
+ },
55
+ "bugs": "https://github.com/COG-GTM/devin-sdk-examples/issues",
56
+ "homepage": "https://github.com/COG-GTM/devin-sdk-examples"
19
57
  }