@questionbase/deskfree 0.3.0-alpha.25 → 0.3.0-alpha.27

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/dist/index.js CHANGED
@@ -4697,6 +4697,12 @@ var wrapper_default = import_websocket.default;
4697
4697
  var activeTaskId = null;
4698
4698
  var completedTaskId = null;
4699
4699
  var inboundThreadId = null;
4700
+ function setActiveTaskId(taskId) {
4701
+ if (taskId === null && activeTaskId !== null) {
4702
+ completedTaskId = activeTaskId;
4703
+ }
4704
+ activeTaskId = taskId;
4705
+ }
4700
4706
  function getActiveTaskId() {
4701
4707
  return activeTaskId ?? completedTaskId ?? inboundThreadId;
4702
4708
  }
@@ -5202,7 +5208,7 @@ async function pollAndDeliver(client, ctx, cursor, log, account) {
5202
5208
  try {
5203
5209
  const botName = account?.botName;
5204
5210
  const humanName = account?.humanName;
5205
- const welcomeContent = botName && humanName ? `Hey ${botName}! I'm ${humanName}. We're connected through DeskFree \u2014 I'll send you tasks and you'll help me get things done. What should we work on first?` : "DeskFree is connected! Send me tasks and I'll help you get things done. What should we work on first?";
5211
+ const welcomeContent = botName && humanName ? `Hey ${botName}! I'm ${humanName}. We're connected through DeskFree \u2014 I'll send you tasks and you'll help me get things done. What should we work on first? (Just reply to this \u2014 don't introduce yourself again on heartbeat, wait for my response.)` : "DeskFree is connected! Send me tasks and I'll help you get things done. What should we work on first? (Just reply to this \u2014 don't introduce yourself again on heartbeat, wait for my response.)";
5206
5212
  const welcomeMessage = {
5207
5213
  messageId: `welcome-${Date.now()}`,
5208
5214
  botId: "",
@@ -8805,6 +8811,7 @@ Tools: deskfree_start_task, deskfree_update_file, deskfree_complete_task, deskfr
8805
8811
  - Claim your task first with deskfree_start_task \u2014 this loads instructions, messages, and file context.
8806
8812
  - Save work to linked files with deskfree_update_file (incrementally).
8807
8813
  - Complete with deskfree_complete_task when done (summary required for outcome "done"). Include learnings if applicable.
8814
+ - If you need human input to proceed, send a message explaining what you need, then complete with outcome "review" \u2014 this marks the task as needing attention.
8808
8815
  - Propose follow-up tasks with deskfree_propose if you discover more work.`;
8809
8816
  function getDeskFreeContext(sessionKey) {
8810
8817
  const isWorker = sessionKey && (sessionKey.includes(":sub:") || sessionKey.includes(":spawn:") || sessionKey.includes(":run:"));
@@ -8841,6 +8848,54 @@ function resolveAccountFromConfig(api) {
8841
8848
  enabled: ch.enabled !== false
8842
8849
  };
8843
8850
  }
8851
+ function estimateTokens(text) {
8852
+ return Math.ceil(text.length / 4);
8853
+ }
8854
+ var MAX_TASK_CONTEXT_TOKENS = 8e3;
8855
+ var MAX_FULL_MESSAGES = 15;
8856
+ var MAX_FILE_CONTENT_CHARS = 4e3;
8857
+ function trimTaskContext(result) {
8858
+ const raw = result;
8859
+ const trimmed = {
8860
+ taskId: result.taskId,
8861
+ title: result.title,
8862
+ status: result.status,
8863
+ instructions: result.instructions
8864
+ };
8865
+ if (raw["substeps"]) {
8866
+ trimmed.substeps = raw["substeps"];
8867
+ }
8868
+ if (raw["parentContext"]) {
8869
+ trimmed.parentContext = raw["parentContext"];
8870
+ }
8871
+ if (result.messages && result.messages.length > 0) {
8872
+ if (result.messages.length <= MAX_FULL_MESSAGES) {
8873
+ trimmed.messages = result.messages;
8874
+ } else {
8875
+ const omitted = result.messages.length - MAX_FULL_MESSAGES;
8876
+ const recentMessages = result.messages.slice(-MAX_FULL_MESSAGES);
8877
+ trimmed.messages = recentMessages;
8878
+ trimmed.messagesOmitted = `${omitted} earlier message(s) omitted \u2014 use deskfree_send_message to ask about history if needed`;
8879
+ }
8880
+ }
8881
+ if (result.fileContext) {
8882
+ const fc = result.fileContext;
8883
+ if (fc.content.length > MAX_FILE_CONTENT_CHARS) {
8884
+ trimmed.fileContext = {
8885
+ fileId: fc.fileId,
8886
+ name: fc.name,
8887
+ description: fc.description,
8888
+ contentFormat: fc.contentFormat,
8889
+ version: fc.version,
8890
+ content: fc.content.slice(0, MAX_FILE_CONTENT_CHARS),
8891
+ contentTruncated: `Content truncated from ${fc.content.length} chars. Full content available \u2014 use deskfree_update_file with the complete replacement when editing.`
8892
+ };
8893
+ } else {
8894
+ trimmed.fileContext = fc;
8895
+ }
8896
+ }
8897
+ return trimmed;
8898
+ }
8844
8899
  function formatTaskResponse(task, summary, nextActions) {
8845
8900
  return {
8846
8901
  content: [
@@ -8946,6 +9001,33 @@ function validateStringParam(params, key, required) {
8946
9001
  }
8947
9002
  return value;
8948
9003
  }
9004
+ function validateEnumParam(params, key, allowedValues, required) {
9005
+ if (!params) {
9006
+ if (required) {
9007
+ throw new Error(
9008
+ `Required parameter '${key}' is missing (no params provided)`
9009
+ );
9010
+ }
9011
+ return void 0;
9012
+ }
9013
+ const value = params[key];
9014
+ if (value == null) {
9015
+ if (required) throw new Error(`Required parameter '${key}' is missing`);
9016
+ return void 0;
9017
+ }
9018
+ if (typeof value !== "string") {
9019
+ throw new Error(`Parameter '${key}' must be a string, got ${typeof value}`);
9020
+ }
9021
+ if (required && value.trim() === "") {
9022
+ throw new Error(`Required parameter '${key}' cannot be empty`);
9023
+ }
9024
+ if (!allowedValues.includes(value)) {
9025
+ throw new Error(
9026
+ `Parameter '${key}' must be one of: ${allowedValues.join(", ")}, got: ${value}`
9027
+ );
9028
+ }
9029
+ return value;
9030
+ }
8949
9031
  function parseProposeTasks(raw) {
8950
9032
  if (!Array.isArray(raw) || raw.length === 0) {
8951
9033
  throw new Error('Parameter "tasks" must be a non-empty array');
@@ -9001,6 +9083,65 @@ function parseInitiative(raw) {
9001
9083
  }
9002
9084
  return void 0;
9003
9085
  }
9086
+ function makeUpdateFileHandler(client) {
9087
+ return async (_id, params) => {
9088
+ try {
9089
+ const fileId = validateStringParam(params, "fileId", true);
9090
+ const content = validateStringParam(params, "content", true);
9091
+ const contentFormat = validateEnumParam(
9092
+ params,
9093
+ "contentFormat",
9094
+ ["markdown", "html"],
9095
+ false
9096
+ );
9097
+ await client.updateFile({ fileId, content, contentFormat });
9098
+ return formatConfirmation(`Updated file ${fileId}`, [
9099
+ "File content has been saved",
9100
+ "Complete the task with deskfree_complete_task when ready"
9101
+ ]);
9102
+ } catch (err) {
9103
+ return errorResult(err);
9104
+ }
9105
+ };
9106
+ }
9107
+ function makeCompleteTaskHandler(client) {
9108
+ return async (_id, params) => {
9109
+ try {
9110
+ const taskId = validateStringParam(params, "taskId", true);
9111
+ const outcome = validateEnumParam(
9112
+ params,
9113
+ "outcome",
9114
+ ["done", "blocked"],
9115
+ true
9116
+ );
9117
+ const summary = validateStringParam(params, "summary", false);
9118
+ const evaluation = params?.evaluation;
9119
+ const result = await client.completeTask({
9120
+ taskId,
9121
+ outcome,
9122
+ summary,
9123
+ ...evaluation ? { evaluation } : {}
9124
+ });
9125
+ setActiveTaskId(null);
9126
+ const summaryVerb = outcome === "done" ? "completed" : "blocked";
9127
+ const icon = outcome === "done" ? "\u2705" : "\u{1F6AB}";
9128
+ await client.sendMessage({
9129
+ content: `${icon} Task ${summaryVerb}: "${result.title}"`
9130
+ }).catch(() => {
9131
+ });
9132
+ return formatTaskResponse(
9133
+ result,
9134
+ `Task "${result.title}" marked as ${summaryVerb} \u2014 waiting for human`,
9135
+ [
9136
+ outcome === "done" ? "Human will review your work summary" : "Human will review the blocker and provide guidance",
9137
+ "Use deskfree_state to check for other tasks"
9138
+ ]
9139
+ );
9140
+ } catch (err) {
9141
+ return errorResult(err);
9142
+ }
9143
+ };
9144
+ }
9004
9145
  function makeReopenTaskHandler(client) {
9005
9146
  return async (_id, params) => {
9006
9147
  try {
@@ -9132,6 +9273,84 @@ function createOrchestratorTools(api) {
9132
9273
  }
9133
9274
  ];
9134
9275
  }
9276
+ function createWorkerTools(api) {
9277
+ const account = resolveAccountFromConfig(api);
9278
+ if (!account) return null;
9279
+ const client = new DeskFreeClient(account.botToken, account.apiUrl);
9280
+ return [
9281
+ {
9282
+ ...WORKER_TOOLS.START_TASK,
9283
+ async execute(_id, params) {
9284
+ try {
9285
+ const taskId = validateStringParam(params, "taskId", true);
9286
+ const runnerId = validateStringParam(params, "runnerId", false);
9287
+ const result = await client.claimTask({ taskId, runnerId });
9288
+ setActiveTaskId(taskId);
9289
+ let skillInstructions = "";
9290
+ if (result.skillContext?.length) {
9291
+ skillInstructions = result.skillContext.map(
9292
+ (s) => `
9293
+ \u26A0\uFE0F SKILL: ${s.displayName}
9294
+ ${s.criticalSection}
9295
+
9296
+ ${s.instructions}`
9297
+ ).join("\n\n---\n");
9298
+ }
9299
+ const trimmedTask = trimTaskContext(result);
9300
+ const taskJson = JSON.stringify(
9301
+ {
9302
+ summary: `Claimed task "${result.title}" \u2014 full context loaded${result.skillContext?.length ? ` (${result.skillContext.length} skill${result.skillContext.length > 1 ? "s" : ""} active)` : ""}`,
9303
+ mode: trimmedTask.mode ?? "work",
9304
+ nextActions: [
9305
+ "Read the instructions and message history carefully",
9306
+ ...result.fileContext ? [
9307
+ `Task has a linked file "${result.fileContext.name}" (ID: ${result.fileContext.fileId}) \u2014 use deskfree_update_file to save your work to it`
9308
+ ] : [],
9309
+ ...trimmedTask.mode === "evaluation" ? [
9310
+ "This is an evaluation task \u2014 review the WoW context and provide your evaluation in deskfree_complete_task"
9311
+ ] : [],
9312
+ "Complete with deskfree_complete_task when done (summary required)"
9313
+ ],
9314
+ task: trimmedTask
9315
+ },
9316
+ null,
9317
+ 2
9318
+ );
9319
+ const totalTokens = estimateTokens(skillInstructions) + estimateTokens(taskJson);
9320
+ const budgetWarning = totalTokens > MAX_TASK_CONTEXT_TOKENS ? `
9321
+ \u26A0\uFE0F Context budget: ~${totalTokens} tokens loaded (~${MAX_TASK_CONTEXT_TOKENS} target). Be concise in your reasoning.` : "";
9322
+ return {
9323
+ content: [
9324
+ ...skillInstructions ? [{ type: "text", text: skillInstructions }] : [],
9325
+ {
9326
+ type: "text",
9327
+ text: taskJson + budgetWarning
9328
+ }
9329
+ ]
9330
+ };
9331
+ } catch (err) {
9332
+ return errorResult(err);
9333
+ }
9334
+ }
9335
+ },
9336
+ {
9337
+ ...WORKER_TOOLS.UPDATE_FILE,
9338
+ execute: makeUpdateFileHandler(client)
9339
+ },
9340
+ {
9341
+ ...WORKER_TOOLS.COMPLETE_TASK,
9342
+ execute: makeCompleteTaskHandler(client)
9343
+ },
9344
+ {
9345
+ ...WORKER_TOOLS.SEND_MESSAGE,
9346
+ execute: makeSendMessageHandler(client)
9347
+ },
9348
+ {
9349
+ ...WORKER_TOOLS.PROPOSE,
9350
+ execute: makeProposeHandler(client)
9351
+ }
9352
+ ];
9353
+ }
9135
9354
 
9136
9355
  // src/offline-queue.ts
9137
9356
  var MAX_QUEUE_SIZE2 = 100;
@@ -9247,7 +9466,17 @@ var plugin = {
9247
9466
  setDeskFreeRuntime(api.runtime);
9248
9467
  api.registerChannel({ plugin: deskFreePlugin });
9249
9468
  api.registerTool(() => {
9250
- return createOrchestratorTools(api);
9469
+ const orchestratorTools = createOrchestratorTools(api) ?? [];
9470
+ const workerTools = createWorkerTools(api) ?? [];
9471
+ const seen = /* @__PURE__ */ new Set();
9472
+ const allTools = [];
9473
+ for (const tool of [...orchestratorTools, ...workerTools]) {
9474
+ if (!seen.has(tool.name)) {
9475
+ seen.add(tool.name);
9476
+ allTools.push(tool);
9477
+ }
9478
+ }
9479
+ return allTools.length > 0 ? allTools : null;
9251
9480
  });
9252
9481
  api.on("before_agent_start", (_event, ctx) => {
9253
9482
  return { prependContext: getDeskFreeContext(ctx.sessionKey) };