@questionbase/deskfree 0.3.0-alpha.26 → 0.3.0-alpha.28

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
@@ -4655,6 +4655,17 @@ async function deliverMessageToAgent(ctx, message, client) {
4655
4655
  replyOptions
4656
4656
  });
4657
4657
  log.info(`Message ${message.messageId} dispatched successfully.`);
4658
+ const activeSession = streamingSession;
4659
+ if (activeSession?.isActive()) {
4660
+ try {
4661
+ await activeSession.close(accumulatedText || void 0);
4662
+ log.info("Streaming reply finalized (post-dispatch cleanup).");
4663
+ } catch (closeErr) {
4664
+ const msg = closeErr instanceof Error ? closeErr.message : String(closeErr);
4665
+ log.warn(`Post-dispatch streaming close failed: ${msg}`);
4666
+ }
4667
+ streamingSession = null;
4668
+ }
4658
4669
  } catch (err) {
4659
4670
  const session = streamingSession;
4660
4671
  if (session?.isActive()) {
@@ -4697,6 +4708,12 @@ var wrapper_default = import_websocket.default;
4697
4708
  var activeTaskId = null;
4698
4709
  var completedTaskId = null;
4699
4710
  var inboundThreadId = null;
4711
+ function setActiveTaskId(taskId) {
4712
+ if (taskId === null && activeTaskId !== null) {
4713
+ completedTaskId = activeTaskId;
4714
+ }
4715
+ activeTaskId = taskId;
4716
+ }
4700
4717
  function getActiveTaskId() {
4701
4718
  return activeTaskId ?? completedTaskId ?? inboundThreadId;
4702
4719
  }
@@ -8788,11 +8805,12 @@ You are the orchestrator. Your job: turn human intent into approved tasks, then
8788
8805
  3. **Dispatch** \u2192 spawn a sub-agent for each approved task. Pass the taskId.
8789
8806
  4. **Communicate** \u2192 \`deskfree_send_message\` for updates outside task threads.
8790
8807
 
8791
- **Bias toward action, not questions.** When a human gives you a direction:
8792
- - Don't ask clarifying questions in the main thread. Propose tasks instead.
8793
- - If the request is clear ("audit my LinkedIn profile"), propose the task directly \u2014 no scoping needed.
8794
- - If the request is ambiguous ("I want to be a LinkedIn influencer"), propose a scoping task first: "Research current state and draft a plan" \u2014 the worker reports back with findings and follow-up proposals.
8795
- - Discovery happens inside tasks, not in conversation. Tasks produce deliverables; questions don't.
8808
+ **Always explain before proposing.** When a human gives you a direction:
8809
+ - First, respond in chat with your understanding and plan \u2014 what you'll do, how you'll break it down, what the deliverables will be.
8810
+ - Then call \`deskfree_propose\` with the concrete tasks. The human sees your thinking first, then the actionable proposal.
8811
+ - If the request is clear ("audit my LinkedIn profile"), a brief "Here's what I'll do:" is enough before proposing.
8812
+ - If the request is ambiguous ("I want to be a LinkedIn influencer"), explain your approach, then propose a scoping task.
8813
+ - Never call \`deskfree_propose\` as your first action \u2014 always write a message first.
8796
8814
 
8797
8815
  You do NOT claim tasks or do work directly. Sub-agents handle execution.
8798
8816
  - When a human writes in a task thread, you receive it with recent context. Use \`deskfree_reopen_task\` if it needs more work.
@@ -8842,6 +8860,54 @@ function resolveAccountFromConfig(api) {
8842
8860
  enabled: ch.enabled !== false
8843
8861
  };
8844
8862
  }
8863
+ function estimateTokens(text) {
8864
+ return Math.ceil(text.length / 4);
8865
+ }
8866
+ var MAX_TASK_CONTEXT_TOKENS = 8e3;
8867
+ var MAX_FULL_MESSAGES = 15;
8868
+ var MAX_FILE_CONTENT_CHARS = 4e3;
8869
+ function trimTaskContext(result) {
8870
+ const raw = result;
8871
+ const trimmed = {
8872
+ taskId: result.taskId,
8873
+ title: result.title,
8874
+ status: result.status,
8875
+ instructions: result.instructions
8876
+ };
8877
+ if (raw["substeps"]) {
8878
+ trimmed.substeps = raw["substeps"];
8879
+ }
8880
+ if (raw["parentContext"]) {
8881
+ trimmed.parentContext = raw["parentContext"];
8882
+ }
8883
+ if (result.messages && result.messages.length > 0) {
8884
+ if (result.messages.length <= MAX_FULL_MESSAGES) {
8885
+ trimmed.messages = result.messages;
8886
+ } else {
8887
+ const omitted = result.messages.length - MAX_FULL_MESSAGES;
8888
+ const recentMessages = result.messages.slice(-MAX_FULL_MESSAGES);
8889
+ trimmed.messages = recentMessages;
8890
+ trimmed.messagesOmitted = `${omitted} earlier message(s) omitted \u2014 use deskfree_send_message to ask about history if needed`;
8891
+ }
8892
+ }
8893
+ if (result.fileContext) {
8894
+ const fc = result.fileContext;
8895
+ if (fc.content.length > MAX_FILE_CONTENT_CHARS) {
8896
+ trimmed.fileContext = {
8897
+ fileId: fc.fileId,
8898
+ name: fc.name,
8899
+ description: fc.description,
8900
+ contentFormat: fc.contentFormat,
8901
+ version: fc.version,
8902
+ content: fc.content.slice(0, MAX_FILE_CONTENT_CHARS),
8903
+ contentTruncated: `Content truncated from ${fc.content.length} chars. Full content available \u2014 use deskfree_update_file with the complete replacement when editing.`
8904
+ };
8905
+ } else {
8906
+ trimmed.fileContext = fc;
8907
+ }
8908
+ }
8909
+ return trimmed;
8910
+ }
8845
8911
  function formatTaskResponse(task, summary, nextActions) {
8846
8912
  return {
8847
8913
  content: [
@@ -8947,6 +9013,33 @@ function validateStringParam(params, key, required) {
8947
9013
  }
8948
9014
  return value;
8949
9015
  }
9016
+ function validateEnumParam(params, key, allowedValues, required) {
9017
+ if (!params) {
9018
+ if (required) {
9019
+ throw new Error(
9020
+ `Required parameter '${key}' is missing (no params provided)`
9021
+ );
9022
+ }
9023
+ return void 0;
9024
+ }
9025
+ const value = params[key];
9026
+ if (value == null) {
9027
+ if (required) throw new Error(`Required parameter '${key}' is missing`);
9028
+ return void 0;
9029
+ }
9030
+ if (typeof value !== "string") {
9031
+ throw new Error(`Parameter '${key}' must be a string, got ${typeof value}`);
9032
+ }
9033
+ if (required && value.trim() === "") {
9034
+ throw new Error(`Required parameter '${key}' cannot be empty`);
9035
+ }
9036
+ if (!allowedValues.includes(value)) {
9037
+ throw new Error(
9038
+ `Parameter '${key}' must be one of: ${allowedValues.join(", ")}, got: ${value}`
9039
+ );
9040
+ }
9041
+ return value;
9042
+ }
8950
9043
  function parseProposeTasks(raw) {
8951
9044
  if (!Array.isArray(raw) || raw.length === 0) {
8952
9045
  throw new Error('Parameter "tasks" must be a non-empty array');
@@ -9002,6 +9095,65 @@ function parseInitiative(raw) {
9002
9095
  }
9003
9096
  return void 0;
9004
9097
  }
9098
+ function makeUpdateFileHandler(client) {
9099
+ return async (_id, params) => {
9100
+ try {
9101
+ const fileId = validateStringParam(params, "fileId", true);
9102
+ const content = validateStringParam(params, "content", true);
9103
+ const contentFormat = validateEnumParam(
9104
+ params,
9105
+ "contentFormat",
9106
+ ["markdown", "html"],
9107
+ false
9108
+ );
9109
+ await client.updateFile({ fileId, content, contentFormat });
9110
+ return formatConfirmation(`Updated file ${fileId}`, [
9111
+ "File content has been saved",
9112
+ "Complete the task with deskfree_complete_task when ready"
9113
+ ]);
9114
+ } catch (err) {
9115
+ return errorResult(err);
9116
+ }
9117
+ };
9118
+ }
9119
+ function makeCompleteTaskHandler(client) {
9120
+ return async (_id, params) => {
9121
+ try {
9122
+ const taskId = validateStringParam(params, "taskId", true);
9123
+ const outcome = validateEnumParam(
9124
+ params,
9125
+ "outcome",
9126
+ ["done", "blocked"],
9127
+ true
9128
+ );
9129
+ const summary = validateStringParam(params, "summary", false);
9130
+ const evaluation = params?.evaluation;
9131
+ const result = await client.completeTask({
9132
+ taskId,
9133
+ outcome,
9134
+ summary,
9135
+ ...evaluation ? { evaluation } : {}
9136
+ });
9137
+ setActiveTaskId(null);
9138
+ const summaryVerb = outcome === "done" ? "completed" : "blocked";
9139
+ const icon = outcome === "done" ? "\u2705" : "\u{1F6AB}";
9140
+ await client.sendMessage({
9141
+ content: `${icon} Task ${summaryVerb}: "${result.title}"`
9142
+ }).catch(() => {
9143
+ });
9144
+ return formatTaskResponse(
9145
+ result,
9146
+ `Task "${result.title}" marked as ${summaryVerb} \u2014 waiting for human`,
9147
+ [
9148
+ outcome === "done" ? "Human will review your work summary" : "Human will review the blocker and provide guidance",
9149
+ "Use deskfree_state to check for other tasks"
9150
+ ]
9151
+ );
9152
+ } catch (err) {
9153
+ return errorResult(err);
9154
+ }
9155
+ };
9156
+ }
9005
9157
  function makeReopenTaskHandler(client) {
9006
9158
  return async (_id, params) => {
9007
9159
  try {
@@ -9133,6 +9285,84 @@ function createOrchestratorTools(api) {
9133
9285
  }
9134
9286
  ];
9135
9287
  }
9288
+ function createWorkerTools(api) {
9289
+ const account = resolveAccountFromConfig(api);
9290
+ if (!account) return null;
9291
+ const client = new DeskFreeClient(account.botToken, account.apiUrl);
9292
+ return [
9293
+ {
9294
+ ...WORKER_TOOLS.START_TASK,
9295
+ async execute(_id, params) {
9296
+ try {
9297
+ const taskId = validateStringParam(params, "taskId", true);
9298
+ const runnerId = validateStringParam(params, "runnerId", false);
9299
+ const result = await client.claimTask({ taskId, runnerId });
9300
+ setActiveTaskId(taskId);
9301
+ let skillInstructions = "";
9302
+ if (result.skillContext?.length) {
9303
+ skillInstructions = result.skillContext.map(
9304
+ (s) => `
9305
+ \u26A0\uFE0F SKILL: ${s.displayName}
9306
+ ${s.criticalSection}
9307
+
9308
+ ${s.instructions}`
9309
+ ).join("\n\n---\n");
9310
+ }
9311
+ const trimmedTask = trimTaskContext(result);
9312
+ const taskJson = JSON.stringify(
9313
+ {
9314
+ summary: `Claimed task "${result.title}" \u2014 full context loaded${result.skillContext?.length ? ` (${result.skillContext.length} skill${result.skillContext.length > 1 ? "s" : ""} active)` : ""}`,
9315
+ mode: trimmedTask.mode ?? "work",
9316
+ nextActions: [
9317
+ "Read the instructions and message history carefully",
9318
+ ...result.fileContext ? [
9319
+ `Task has a linked file "${result.fileContext.name}" (ID: ${result.fileContext.fileId}) \u2014 use deskfree_update_file to save your work to it`
9320
+ ] : [],
9321
+ ...trimmedTask.mode === "evaluation" ? [
9322
+ "This is an evaluation task \u2014 review the WoW context and provide your evaluation in deskfree_complete_task"
9323
+ ] : [],
9324
+ "Complete with deskfree_complete_task when done (summary required)"
9325
+ ],
9326
+ task: trimmedTask
9327
+ },
9328
+ null,
9329
+ 2
9330
+ );
9331
+ const totalTokens = estimateTokens(skillInstructions) + estimateTokens(taskJson);
9332
+ const budgetWarning = totalTokens > MAX_TASK_CONTEXT_TOKENS ? `
9333
+ \u26A0\uFE0F Context budget: ~${totalTokens} tokens loaded (~${MAX_TASK_CONTEXT_TOKENS} target). Be concise in your reasoning.` : "";
9334
+ return {
9335
+ content: [
9336
+ ...skillInstructions ? [{ type: "text", text: skillInstructions }] : [],
9337
+ {
9338
+ type: "text",
9339
+ text: taskJson + budgetWarning
9340
+ }
9341
+ ]
9342
+ };
9343
+ } catch (err) {
9344
+ return errorResult(err);
9345
+ }
9346
+ }
9347
+ },
9348
+ {
9349
+ ...WORKER_TOOLS.UPDATE_FILE,
9350
+ execute: makeUpdateFileHandler(client)
9351
+ },
9352
+ {
9353
+ ...WORKER_TOOLS.COMPLETE_TASK,
9354
+ execute: makeCompleteTaskHandler(client)
9355
+ },
9356
+ {
9357
+ ...WORKER_TOOLS.SEND_MESSAGE,
9358
+ execute: makeSendMessageHandler(client)
9359
+ },
9360
+ {
9361
+ ...WORKER_TOOLS.PROPOSE,
9362
+ execute: makeProposeHandler(client)
9363
+ }
9364
+ ];
9365
+ }
9136
9366
 
9137
9367
  // src/offline-queue.ts
9138
9368
  var MAX_QUEUE_SIZE2 = 100;
@@ -9248,7 +9478,17 @@ var plugin = {
9248
9478
  setDeskFreeRuntime(api.runtime);
9249
9479
  api.registerChannel({ plugin: deskFreePlugin });
9250
9480
  api.registerTool(() => {
9251
- return createOrchestratorTools(api);
9481
+ const orchestratorTools = createOrchestratorTools(api) ?? [];
9482
+ const workerTools = createWorkerTools(api) ?? [];
9483
+ const seen = /* @__PURE__ */ new Set();
9484
+ const allTools = [];
9485
+ for (const tool of [...orchestratorTools, ...workerTools]) {
9486
+ if (!seen.has(tool.name)) {
9487
+ seen.add(tool.name);
9488
+ allTools.push(tool);
9489
+ }
9490
+ }
9491
+ return allTools.length > 0 ? allTools : null;
9252
9492
  });
9253
9493
  api.on("before_agent_start", (_event, ctx) => {
9254
9494
  return { prependContext: getDeskFreeContext(ctx.sessionKey) };