@dana4/sdk 0.1.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -147,6 +147,7 @@ Every handler gets a `Context` that is already bound to its workspace, channel a
147
147
  | `ctx.docs.append(path, content)` | Append to a document. |
148
148
  | `ctx.docs.edit(path, old, new, replaceAll?)` | Exact-string replace; returns the number of replacements. |
149
149
  | `ctx.plan.set / add / complete` | Manage the channel's plan, a markdown checklist. |
150
+ | `ctx.memory.set(memory)` | Replace the channel's memory, markdown notes shared by its agents and people. |
150
151
  | `ctx.assets.upload(data, { name, ext })` | Store a file. Put the returned `url` (`dana4-asset://…`) in content. |
151
152
  | `ctx.progress(fraction, message?)` | Report progress (0–1) to people watching the task. |
152
153
  | `ctx.cost({ usd?, credits?, detail? })` | Record spend, e.g. model usage. It adds up and is charged once, when the task ends. |
@@ -175,7 +176,7 @@ export const agent = defineAgent({
175
176
  onChat: async ({ message, thread }, ctx) => {
176
177
  const recent = (await ctx.client.chat.messages(message.channel_id)).messages;
177
178
  const result = await generateText({
178
- model: anthropic("claude-sonnet-5-5"),
179
+ model: anthropic("claude-haiku-5-5"),
179
180
  system: threadInstructions("You are a helpful assistant in a Dana4 workspace.", thread),
180
181
  messages: [
181
182
  ...chatHistory(recent, { me: ctx.me, exclude: message.id }),
@@ -190,9 +191,10 @@ export const agent = defineAgent({
190
191
  });
191
192
  ```
192
193
 
194
+ - `ctx.tickets.create({ title, description?, status?, blocked_by? })` (and the `create_ticket` tool) puts a ticket on the board; tickets are referred to by their per-workspace number, so `blocked_by: [12]` means #12.
193
195
  - `dana4Tools(ctx, { exclude, delegation })`: drop individual tools with `exclude: ["send_message", …]`, or turn off the per-capability delegation tools with `delegation: false`.
194
196
  - `chatHistory(messages, { me, exclude })` converts a thread into model messages, oldest first. Your agent's messages become assistant turns; everyone else's become user turns prefixed with the author's name.
195
- - `threadInstructions(base, thread)` appends the thread's topics, intent and plan to your system prompt.
197
+ - `threadInstructions(base, thread)` appends the thread's topics, intent, plan and memory to your system prompt.
196
198
 
197
199
  ## Running your agent
198
200
 
@@ -250,7 +252,7 @@ Set the key with `wrangler secret put DANA4_API_KEY`. Set `DANA4_URL` only if yo
250
252
 
251
253
  ## The low-level client
252
254
 
253
- `Dana4Client` wraps every endpoint of the Dana4 SDK API, grouped by resource: `agents`, `tasks`, `docs`, `chat`, `workspaces`, `automations`, `workflows` and `assets`. Use it for scripts, or for integrations that aren't long-running agents.
255
+ `Dana4Client` wraps every endpoint of the Dana4 SDK API, grouped by resource: `agents`, `tasks`, `docs`, `chat`, `workspaces`, `tickets`, `automations`, `workflows` and `assets`. Use it for scripts, or for integrations that aren't long-running agents.
254
256
 
255
257
  ```ts
256
258
  import { Dana4Client } from "@dana4/sdk";
package/dist/agent.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { ChatThread, Dana4Client, Document, DocumentType, Json, JsonObject, Message, MessageCategory, PresenceActivity, TakenTask, UpdateTaskPayload } from "./client.js";
2
+ import type { ChatThread, CreateTicketBody, Dana4Client, Document, DocumentType, Json, JsonObject, Message, MessageCategory, PresenceActivity, TakenTask, UpdateTaskPayload } from "./client.js";
3
3
  /** A unit of work exposed to Dana4: workflows assign it, chat agents call it.
4
4
  * Return the step's result (any JSON) to complete the task; throw to fail it. */
5
5
  export type Capability<I extends z.ZodObject = z.ZodObject, R = unknown> = {
@@ -88,6 +88,16 @@ export declare class Context {
88
88
  }): Promise<Message>;
89
89
  /** Workspace params (API keys, feature flags, …) set in workspace settings. */
90
90
  params(): Promise<JsonObject>;
91
+ readonly tickets: {
92
+ /** Put a ticket on this workspace's board; `blocked_by` takes ticket numbers. */
93
+ create: (ticket: Omit<CreateTicketBody, "workspace_id">) => Promise<{
94
+ blocked_by: number[];
95
+ number: number;
96
+ status: import("./schema.js").components["schemas"]["BoardStatus"];
97
+ title: string;
98
+ workspace_id: string;
99
+ }>;
100
+ };
91
101
  readonly docs: {
92
102
  /** Read by path; shows this agent as `activity` on the document. */
93
103
  get: (path: string, activity?: PresenceActivity) => Promise<Document>;
@@ -143,9 +153,11 @@ export declare class Context {
143
153
  direct?: boolean;
144
154
  document_id?: string | null;
145
155
  intent?: string | null;
156
+ memory?: string | null;
146
157
  messages: import("./schema.js").components["schemas"]["Message"][];
147
158
  parent_channel_id?: string | null;
148
159
  plan?: string | null;
160
+ tickets?: import("./schema.js").components["schemas"]["TicketSummary"][];
149
161
  topics: import("./schema.js").components["schemas"]["TopicSummary"][];
150
162
  }[]>;
151
163
  create: (doc: {
@@ -166,6 +178,7 @@ export declare class Context {
166
178
  document_id: string;
167
179
  id: string;
168
180
  intent?: string | null;
181
+ memory?: string | null;
169
182
  owners: string[];
170
183
  parent_channel_id?: string | null;
171
184
  parent_message_id?: string | null;
@@ -180,6 +193,7 @@ export declare class Context {
180
193
  document_id: string;
181
194
  id: string;
182
195
  intent?: string | null;
196
+ memory?: string | null;
183
197
  owners: string[];
184
198
  parent_channel_id?: string | null;
185
199
  parent_message_id?: string | null;
@@ -194,6 +208,25 @@ export declare class Context {
194
208
  document_id: string;
195
209
  id: string;
196
210
  intent?: string | null;
211
+ memory?: string | null;
212
+ owners: string[];
213
+ parent_channel_id?: string | null;
214
+ parent_message_id?: string | null;
215
+ plan?: string | null;
216
+ plan_completed: boolean;
217
+ private: boolean;
218
+ time: import("./schema.js").components["schemas"]["Timestamps"];
219
+ title: string;
220
+ workspace_id: string;
221
+ }>;
222
+ };
223
+ /** The channel's memory: markdown notes shared by its agents and people. Answers the updated channel. */
224
+ readonly memory: {
225
+ set: (memory: string, channelId?: string | null) => Promise<{
226
+ document_id: string;
227
+ id: string;
228
+ intent?: string | null;
229
+ memory?: string | null;
197
230
  owners: string[];
198
231
  parent_channel_id?: string | null;
199
232
  parent_message_id?: string | null;
@@ -219,18 +252,21 @@ export declare class Context {
219
252
  /** Report progress (0–1) and optionally a message people see in the channel. */
220
253
  progress(progress: number, message?: string): Promise<void>;
221
254
  /** Record spend. It accumulates and is charged once, when the task ends. */
222
- cost({ usd, credits, detail }: {
255
+ cost({ usd, credits, detail, }: {
223
256
  usd?: number;
224
257
  credits?: number;
225
258
  detail?: JsonObject;
226
259
  }): void;
260
+ /** Record tools the LLM called, for debugging. Saved on the task when it
261
+ * ends. By convention `{ name, args }`, plus `error` when one failed. */
262
+ toolCalls(...calls: JsonObject[]): void;
227
263
  /** Queue another agent's capability. It runs asynchronously and posts its
228
264
  * result back here, @-mentioning this agent. Answers the new task's id. */
229
265
  delegate(agent: string, capability: string, inputs?: JsonObject): Promise<string>;
230
266
  /** The result of an earlier step in this task's journey, or null if not ready. */
231
267
  stepResult(stepId: string): Promise<Json>;
232
- /** @internal The accumulated cost, in `PATCH /tasks/{id}` shape. */
233
- settle(): Pick<UpdateTaskPayload, "cost_usd" | "credits_cost" | "cost_details">;
268
+ /** @internal The accumulated cost and tool calls, in `PATCH /tasks/{id}` shape. */
269
+ settle(): Pick<UpdateTaskPayload, "cost_usd" | "credits_cost" | "cost_details" | "tool_calls">;
234
270
  }
235
271
  /** What `onJoin` gets: there is no task yet, so no document writes. */
236
272
  export type JoinContext = Pick<Context, "client" | "me" | "workspaceId" | "send" | "params">;
package/dist/agent.js CHANGED
@@ -7,7 +7,9 @@ const BASE_FIELDS = ["workspace_id", "task_id", "channel_id", "tag_usernames"];
7
7
  * payload keys a schema declares, so the base fields must be declared too. */
8
8
  export function capabilitySchema(key, cap) {
9
9
  // Input mode: a field with a default is optional to whoever calls it.
10
- const { $schema: _, ...schema } = z.toJSONSchema(cap.input, { io: "input" });
10
+ const { $schema: _, ...schema } = z.toJSONSchema(cap.input, {
11
+ io: "input",
12
+ });
11
13
  return {
12
14
  ...schema,
13
15
  title: cap.title ?? key,
@@ -22,7 +24,7 @@ export function capabilitySchema(key, cap) {
22
24
  required: [...(schema.required ?? []), "workspace_id", "task_id"],
23
25
  };
24
26
  }
25
- const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
27
+ const errorMessage = (e) => e instanceof Error ? e.message : String(e);
26
28
  /** Usernames @-mentioned in `body`, lowercased; the server's `extract_tagged_usernames`. */
27
29
  const taggedUsernames = (body) => body
28
30
  .split(/\s+/)
@@ -43,6 +45,7 @@ export class Context {
43
45
  #usd = 0;
44
46
  #credits = 0;
45
47
  #costDetails = [];
48
+ #toolCalls = [];
46
49
  constructor(client,
47
50
  /** This agent's username. */
48
51
  me, workspaceId,
@@ -78,6 +81,10 @@ export class Context {
78
81
  params() {
79
82
  return this.client.workspaces.params(this.workspaceId);
80
83
  }
84
+ tickets = {
85
+ /** Put a ticket on this workspace's board; `blocked_by` takes ticket numbers. */
86
+ create: (ticket) => this.client.tickets.create({ ...ticket, workspace_id: this.workspaceId }),
87
+ };
81
88
  docs = {
82
89
  /** Read by path; shows this agent as `activity` on the document. */
83
90
  get: async (path, activity = "reading") => {
@@ -101,8 +108,13 @@ export class Context {
101
108
  replace: (path, content) => this.client.docs.replace({ path, content, task_id: this.#task() }),
102
109
  append: (path, content) => this.client.docs.append({ path, content, task_id: this.#task() }),
103
110
  /** Exact-string replace, applied server-side. Answers the replacement count. */
104
- edit: async (path, oldString, newString, replaceAll = false) => (await this.client.docs.edit({ path, old_string: oldString, new_string: newString, replace_all: replaceAll, task_id: this.#task() }))
105
- .replacements,
111
+ edit: async (path, oldString, newString, replaceAll = false) => (await this.client.docs.edit({
112
+ path,
113
+ old_string: oldString,
114
+ new_string: newString,
115
+ replace_all: replaceAll,
116
+ task_id: this.#task(),
117
+ })).replacements,
106
118
  };
107
119
  /** The channel's plan: a markdown checklist. Each call answers the updated channel. */
108
120
  plan = {
@@ -110,6 +122,10 @@ export class Context {
110
122
  complete: (item, channelId = this.channelId) => this.client.chat.completePlanItem(this.#channel(channelId), item),
111
123
  add: (item, channelId = this.channelId) => this.client.chat.addPlanItem(this.#channel(channelId), item),
112
124
  };
125
+ /** The channel's memory: markdown notes shared by its agents and people. Answers the updated channel. */
126
+ memory = {
127
+ set: (memory, channelId = this.channelId) => this.client.chat.setMemory(this.#channel(channelId), memory),
128
+ };
113
129
  #channel(channelId) {
114
130
  if (!channelId)
115
131
  throw new Error("No channel: pass channelId explicitly");
@@ -121,15 +137,24 @@ export class Context {
121
137
  };
122
138
  /** Report progress (0–1) and optionally a message people see in the channel. */
123
139
  async progress(progress, message) {
124
- await this.client.tasks.update(this.#task(), { status: "running", progress, message });
140
+ await this.client.tasks.update(this.#task(), {
141
+ status: "running",
142
+ progress,
143
+ message,
144
+ });
125
145
  }
126
146
  /** Record spend. It accumulates and is charged once, when the task ends. */
127
- cost({ usd = 0, credits = 0, detail }) {
147
+ cost({ usd = 0, credits = 0, detail, }) {
128
148
  this.#usd += usd;
129
149
  this.#credits += credits;
130
150
  if (detail)
131
151
  this.#costDetails.push(detail);
132
152
  }
153
+ /** Record tools the LLM called, for debugging. Saved on the task when it
154
+ * ends. By convention `{ name, args }`, plus `error` when one failed. */
155
+ toolCalls(...calls) {
156
+ this.#toolCalls.push(...calls);
157
+ }
133
158
  /** Queue another agent's capability. It runs asynchronously and posts its
134
159
  * result back here, @-mentioning this agent. Answers the new task's id. */
135
160
  delegate(agent, capability, inputs = {}) {
@@ -138,19 +163,24 @@ export class Context {
138
163
  task_id: this.#task(),
139
164
  channel_id: this.channelId,
140
165
  inputs,
141
- metadata: { triggered_by: this.me, triggered_from: "chat", tag_usernames: [this.me] },
166
+ metadata: {
167
+ triggered_by: this.me,
168
+ triggered_from: "chat",
169
+ tag_usernames: [this.me],
170
+ },
142
171
  });
143
172
  }
144
173
  /** The result of an earlier step in this task's journey, or null if not ready. */
145
174
  stepResult(stepId) {
146
175
  return this.client.tasks.stepResult(this.#task(), stepId);
147
176
  }
148
- /** @internal The accumulated cost, in `PATCH /tasks/{id}` shape. */
177
+ /** @internal The accumulated cost and tool calls, in `PATCH /tasks/{id}` shape. */
149
178
  settle() {
150
179
  return {
151
180
  cost_usd: this.#usd || undefined,
152
181
  credits_cost: this.#credits || undefined,
153
182
  cost_details: this.#costDetails.length ? this.#costDetails : undefined,
183
+ tool_calls: this.#toolCalls.length ? this.#toolCalls : undefined,
154
184
  };
155
185
  }
156
186
  }
@@ -177,7 +207,10 @@ export class AgentRuntime {
177
207
  version,
178
208
  capabilities: Object.fromEntries(Object.entries(capabilities).map(([key, cap]) => [
179
209
  key,
180
- { input_schema: capabilitySchema(key, cap), show_in_chat: cap.showInChat ?? true },
210
+ {
211
+ input_schema: capabilitySchema(key, cap),
212
+ show_in_chat: cap.showInChat ?? true,
213
+ },
181
214
  ])),
182
215
  },
183
216
  });
@@ -192,7 +225,11 @@ export class AgentRuntime {
192
225
  /** Put a task interrupted by a crash back in the queue. Never throws. */
193
226
  async requeue(taskId) {
194
227
  await this.client.tasks
195
- .update(taskId, { status: "retry", progress: 0, message: "Agent restarted; re-queuing task" })
228
+ .update(taskId, {
229
+ status: "retry",
230
+ progress: 0,
231
+ message: "Agent restarted; re-queuing task",
232
+ })
196
233
  .catch((e) => this.log.error("dana4: re-queuing failed:", errorMessage(e)));
197
234
  }
198
235
  /** Take the next task and chat thread. A taken task is acked (moved to
@@ -202,7 +239,10 @@ export class AgentRuntime {
202
239
  try {
203
240
  const taken = await this.client.tasks.take();
204
241
  if (taken) {
205
- await this.client.tasks.update(taken.task.id, { status: "running", progress: 0 });
242
+ await this.client.tasks.update(taken.task.id, {
243
+ status: "running",
244
+ progress: 0,
245
+ });
206
246
  work.push({ kind: "task", taken });
207
247
  }
208
248
  }
@@ -230,7 +270,9 @@ export class AgentRuntime {
230
270
  const me = this.me.toLowerCase();
231
271
  return theirs
232
272
  .filter((m) => taggedUsernames(m.message).includes(me))
233
- .map((m) => m.category === "invited_to_workspace" ? { kind: "join", workspaceId: m.workspace_id } : { kind: "chat", message: m, thread });
273
+ .map((m) => m.category === "invited_to_workspace"
274
+ ? { kind: "join", workspaceId: m.workspace_id }
275
+ : { kind: "chat", message: m, thread });
234
276
  }
235
277
  /** Do one piece of work. Never throws: failures close the task as failed.
236
278
  * `save` gets the work back whenever it gains state a crash must not lose (a
@@ -250,7 +292,11 @@ export class AgentRuntime {
250
292
  }
251
293
  }
252
294
  async #runTask({ task, payload }) {
253
- const fail = (message, ctx) => this.client.tasks.update(task.id, { status: "failed", message, ...ctx?.settle() });
295
+ const fail = (message, ctx) => this.client.tasks.update(task.id, {
296
+ status: "failed",
297
+ message,
298
+ ...ctx?.settle(),
299
+ });
254
300
  const step = task.step_type;
255
301
  const spec = typeof step === "object" && "agent" in step ? step.agent : null;
256
302
  if (!spec || spec.name !== this.me)
@@ -262,12 +308,17 @@ export class AgentRuntime {
262
308
  const parsed = cap.input.safeParse(own);
263
309
  if (!parsed.success)
264
310
  return void (await fail(`Invalid input for ${spec.capability}: ${z.prettifyError(parsed.error)}`));
265
- const ctx = new Context(this.client, this.me, String(payload.workspace_id), typeof payload.channel_id === "string" ? payload.channel_id : task.channel_id, task.id, Array.isArray(payload.tag_usernames) ? payload.tag_usernames.map(String) : []);
311
+ const ctx = new Context(this.client, this.me, String(payload.workspace_id), typeof payload.channel_id === "string"
312
+ ? payload.channel_id
313
+ : task.channel_id, task.id, Array.isArray(payload.tag_usernames)
314
+ ? payload.tag_usernames.map(String)
315
+ : []);
266
316
  let result;
267
317
  try {
268
318
  result = await cap.run(parsed.data, ctx);
269
319
  }
270
320
  catch (e) {
321
+ this.log.error(`dana4: ${spec.capability} failed:`, e);
271
322
  return void (await fail(errorMessage(e), ctx));
272
323
  }
273
324
  await this.client.tasks.update(task.id, {
@@ -284,7 +335,11 @@ export class AgentRuntime {
284
335
  const { message, thread } = work;
285
336
  let taskId = work.taskId;
286
337
  if (!taskId) {
287
- taskId = await this.client.tasks.start({ step_name: "read_message", workspace_id: message.workspace_id, channel_id: message.channel_id });
338
+ taskId = await this.client.tasks.start({
339
+ step_name: "read_message",
340
+ workspace_id: message.workspace_id,
341
+ channel_id: message.channel_id,
342
+ });
288
343
  save?.({ ...work, taskId });
289
344
  }
290
345
  const ctx = new Context(this.client, this.me, message.workspace_id, message.channel_id, taskId, [this.me]);
@@ -293,12 +348,21 @@ export class AgentRuntime {
293
348
  if (thinking)
294
349
  await ctx.send(thinking, { category: "transient" });
295
350
  await this.agent.onChat({ message, thread }, ctx);
296
- await this.client.tasks.update(taskId, { status: "completed", progress: 1, ...ctx.settle() });
351
+ await this.client.tasks.update(taskId, {
352
+ status: "completed",
353
+ progress: 1,
354
+ ...ctx.settle(),
355
+ });
297
356
  }
298
357
  catch (e) {
299
358
  // The server posts a failed task's message into its channel, so the
300
- // person who asked sees it.
301
- await this.client.tasks.update(taskId, { status: "failed", message: errorMessage(e), ...ctx.settle() });
359
+ // person who asked sees it. Log it too, with the stack, for the host.
360
+ this.log.error("dana4: chat failed:", e);
361
+ await this.client.tasks.update(taskId, {
362
+ status: "failed",
363
+ message: errorMessage(e),
364
+ ...ctx.settle(),
365
+ });
302
366
  }
303
367
  }
304
368
  }
package/dist/ai.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare function insertAt(document: string, line: number, content: string
16
16
  /** A capability's input schema as a tool schema: only primitive params (the
17
17
  * platform resolves documents and the like), and never the ids it injects. */
18
18
  export declare function capabilityToolSchema(schema: JsonObject | null | undefined): JsonObject;
19
- export type Dana4ToolName = "list_documents" | "grep_document" | "search_documents" | "read_document" | "get_step_result" | "create_document" | "write_document" | "append_document" | "edit_document" | "insert_in_document" | "send_message" | "set_plan" | "complete_plan_item" | "add_plan_item";
19
+ export type Dana4ToolName = "list_documents" | "grep_document" | "search_documents" | "read_document" | "get_step_result" | "create_document" | "write_document" | "append_document" | "edit_document" | "insert_in_document" | "send_message" | "set_plan" | "complete_plan_item" | "add_plan_item" | "set_memory" | "create_ticket";
20
20
  /** Tools for reading and writing the workspace's documents, messaging, and the
21
21
  * thread's plan — plus, unless `delegation: false`, one `<agent>__<capability>`
22
22
  * tool per capability other agents in the workspace expose. Tool errors go back
@@ -32,6 +32,6 @@ export declare function chatHistory(messages: Message[], { me, exclude }: {
32
32
  me: string;
33
33
  exclude?: string;
34
34
  }): ModelMessage[];
35
- /** `base` followed by the thread's topics, intent and plan, so the model knows
36
- * what the conversation is for. */
37
- export declare function threadInstructions(base: string, thread: Pick<ChatThread, "topics" | "intent" | "plan">): string;
35
+ /** `base` followed by the thread's topics, the board, intent, plan and memory, so the
36
+ * model knows what the conversation is for. */
37
+ export declare function threadInstructions(base: string, thread: Pick<ChatThread, "topics" | "tickets" | "intent" | "plan" | "memory">): string;