palmier 1.1.25 → 1.1.26

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
@@ -53,7 +53,6 @@ Every task Palmier runs gets these capabilities automatically. Their usage is wr
53
53
  | Capability | Description |
54
54
  |------------|-------------|
55
55
  | `notify` | Send a push notification to the user's device |
56
- | `request-choice` | Ask the user a question with a fixed set of options and return the one picked; a single option serves as a confirmation (blocks until response; aborting aborts the task) |
57
56
  | `fill-password` | Fill a saved password or PIN into the active browser session and submit the form, prompting the user if unknown (the secret is never revealed to the agent) |
58
57
  | `device-geolocation` | Get GPS location of the user's mobile device |
59
58
  | `read-contacts` | Read the contact list from the user's device |
@@ -76,7 +75,7 @@ All device tools work while the Palmier Android app is in the background — the
76
75
 
77
76
  ### Asking the User
78
77
 
79
- When an agent needs an answer it can't get from a fixed list, it writes its question as its final message and ends with the `[PALMIER_TASK_NEED_INPUT]` marker. The agent CLI then exits and the run moves to a "needs input" state, with a push notification that opens the run. The user answers in the run view's message box, or aborts from there.
78
+ When an agent needs something from the user, it writes its question as its final message and ends with the `[PALMIER_TASK_NEED_INPUT]` marker. The agent CLI then exits and the run moves to a "needs input" state, with a push notification that opens the run. The user answers in the run view's message box, or aborts from there.
80
79
 
81
80
  `palmier run` stays alive during the wait, so anything the agent left running, such as its browser, stays open. Palmier polls for the reply and resumes the same agent session with it, repeating until the agent reports success or failure. An unanswered question fails the run after 24 hours. The reply is handed over through a file in the run directory, so a daemon restart doesn't lose the wait.
82
81
 
@@ -2,7 +2,6 @@ import { registerPending } from "./pending-requests.js";
2
2
  import { getLinkedDevice } from "./linked-device.js";
3
3
  import { getNotifications } from "./notification-store.js";
4
4
  import { getSmsMessages } from "./sms-store.js";
5
- import { getTaskDir, readHistory, spliceUserMessage } from "./task.js";
6
5
  import { lookupPassword, savePassword, normalizeOrigin } from "./password-store.js";
7
6
  import { fillPasswordInBrowser } from "./playwright-fill.js";
8
7
  export class ToolError extends Error {
@@ -41,27 +40,6 @@ const notifyTool = {
41
40
  throw new ToolError(result.error ?? "Push notification failed", 502);
42
41
  },
43
42
  };
44
- /** Splice the user's answer into the run file so history shows it even if the agent never echoes it. */
45
- function recordUserResponseToRun(ctx, content) {
46
- const taskId = ctx.taskId;
47
- const taskDir = getTaskDir(ctx.config.projectRoot, taskId);
48
- const { entries } = readHistory(ctx.config.projectRoot, { task_id: taskId, limit: 1 });
49
- const runId = entries[0]?.run_id;
50
- if (!runId)
51
- return;
52
- try {
53
- spliceUserMessage(taskDir, runId, {
54
- role: "user",
55
- time: Date.now(),
56
- content,
57
- type: "input",
58
- });
59
- void ctx.publishEvent(taskId, { event_type: "result-updated", run_id: runId });
60
- }
61
- catch {
62
- // Run file missing or unwritable — best-effort, do not fail the tool call.
63
- }
64
- }
65
43
  export const requestConfirmationTool = {
66
44
  name: "request-confirmation",
67
45
  description: [
@@ -80,7 +58,7 @@ export const requestConfirmationTool = {
80
58
  const { description } = args;
81
59
  if (!description)
82
60
  throw new ToolError("description is required", 400);
83
- const pendingPromise = registerPending(ctx.taskId, "confirmation", undefined, {
61
+ const pendingPromise = registerPending(ctx.taskId, "confirmation", {
84
62
  session_id: ctx.taskId,
85
63
  description,
86
64
  });
@@ -103,66 +81,6 @@ export const requestConfirmationTool = {
103
81
  return { confirmed };
104
82
  },
105
83
  };
106
- const requestChoiceTool = {
107
- name: "request-choice",
108
- description: [
109
- "Ask the user a question with a fixed set of answers and let them pick one.",
110
- "The request blocks until the user picks an option or aborts the task.",
111
- 'Response: `{"choice": "<option>"}` with the selected option, or `{"aborted": true}` if the user aborts.',
112
- "Use this whenever the possible answers are known in advance.",
113
- "An abort option that ends the task is always shown, so to ask for confirmation before continuing, pass a single option naming the action (e.g. [\"Send email\"]). Add a decline option only if declining should let the task continue differently.",
114
- ],
115
- inputSchema: {
116
- type: "object",
117
- properties: {
118
- question: { type: "string", description: "The question to ask" },
119
- options: {
120
- type: "array",
121
- items: { type: "string" },
122
- description: 'Unique option labels, in display order (e.g. ["Reply now", "Reply later"])',
123
- minItems: 1,
124
- },
125
- },
126
- required: ["question", "options"],
127
- },
128
- async handler(args, ctx) {
129
- const { question, options } = args;
130
- if (!question)
131
- throw new ToolError("question is required", 400);
132
- if (!Array.isArray(options) || options.length < 1 || options.some((o) => typeof o !== "string" || !o.trim())) {
133
- throw new ToolError("options must contain at least 1 non-empty string", 400);
134
- }
135
- if (new Set(options).size !== options.length)
136
- throw new ToolError("options must be unique", 400);
137
- const pendingPromise = registerPending(ctx.taskId, "choice", options, {
138
- session_id: ctx.taskId,
139
- description: question,
140
- });
141
- await ctx.publishEvent("_choice", {
142
- event_type: "choice-request",
143
- host_id: ctx.config.hostId,
144
- session_id: ctx.taskId,
145
- description: question,
146
- options,
147
- });
148
- // Clients answer with the option index so no label can collide with the "aborted" sentinel.
149
- const response = await pendingPromise;
150
- const choice = /^\d+$/.test(response[0] ?? "") ? options[Number(response[0])] : undefined;
151
- const aborted = choice === undefined;
152
- recordUserResponseToRun(ctx, aborted ? "Aborted" : `**${question}**\n\n${choice}`);
153
- await ctx.publishEvent("_choice", {
154
- event_type: "choice-resolved",
155
- host_id: ctx.config.hostId,
156
- session_id: ctx.taskId,
157
- status: aborted ? "aborted" : "chosen",
158
- });
159
- if (aborted) {
160
- await ctx.abortTask();
161
- return { aborted: true };
162
- }
163
- return { choice };
164
- },
165
- };
166
84
  const fillPasswordTool = {
167
85
  name: "fill-password",
168
86
  description: [
@@ -190,26 +108,21 @@ const fillPasswordTool = {
190
108
  const origin = normalizeOrigin(url);
191
109
  let password = lookupPassword(origin, username);
192
110
  if (password === undefined) {
193
- const question = `Password or PIN for ${username} on ${origin}`;
194
111
  const description = `Enter the password or PIN to sign in as ${username} on ${origin}.`;
195
- const pendingPromise = registerPending(ctx.taskId, "input", [question], {
112
+ const pendingPromise = registerPending(ctx.taskId, "password", {
196
113
  session_id: ctx.taskId,
197
114
  description,
198
- input_questions: [question],
199
- secret: true,
200
115
  });
201
- await ctx.publishEvent("_input", {
202
- event_type: "input-request",
116
+ await ctx.publishEvent("_password", {
117
+ event_type: "password-request",
203
118
  host_id: ctx.config.hostId,
204
119
  session_id: ctx.taskId,
205
120
  description,
206
- input_questions: [question],
207
- secret: true,
208
121
  });
209
122
  const response = await pendingPromise;
210
123
  const aborted = response.length === 1 && response[0] === "aborted";
211
- await ctx.publishEvent("_input", {
212
- event_type: "input-resolved", host_id: ctx.config.hostId,
124
+ await ctx.publishEvent("_password", {
125
+ event_type: "password-resolved", host_id: ctx.config.hostId,
213
126
  session_id: ctx.taskId, status: aborted ? "aborted" : "provided",
214
127
  });
215
128
  // Never record the secret to the task run file.
@@ -218,8 +131,7 @@ const fillPasswordTool = {
218
131
  return { aborted: true };
219
132
  }
220
133
  password = response[0];
221
- // The PWA appends a "save"/"nosave" flag for password prompts; persist
222
- // unless the user opted out via the dialog's checkbox.
134
+ // The PWA appends a "save"/"nosave" flag; persist unless the user opted out.
223
135
  if (response[1] !== "nosave")
224
136
  savePassword(origin, username, password);
225
137
  }
@@ -722,7 +634,7 @@ const sendEmailTool = {
722
634
  return result;
723
635
  },
724
636
  };
725
- export const agentTools = [notifyTool, requestChoiceTool, fillPasswordTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlarmTool, readBatteryTool, setRingerModeTool];
637
+ export const agentTools = [notifyTool, fillPasswordTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlarmTool, readBatteryTool, setRingerModeTool];
726
638
  export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
727
639
  const deviceNotificationsResource = {
728
640
  name: "Device Notifications",
@@ -25,9 +25,9 @@ Do this at the end of every response, including responses to follow-up messages.
25
25
 
26
26
  ## Asking the User
27
27
 
28
- When you need information from the user (answers to questions, preferences, clarifications, etc.), do not guess or fail. Write your questions in full as your final message, then end it with `[PALMIER_TASK_NEED_INPUT]`. Your process will exit, and the user's reply will arrive as the next message in this same session. Anything you left running, such as a browser, stays open, so leave it as it is.
28
+ When you need something from the user (answers to questions, preferences, confirmation before acting, clarifications, etc.), do not guess or fail. Write your questions in full as your final message, then end it with `[PALMIER_TASK_NEED_INPUT]`. Your process will exit, and the user's reply will arrive as the next message in this same session. Anything you left running, such as a browser, stays open, so leave it as it is.
29
29
 
30
- If the possible answers are known in advance, use the `request-choice` endpoint instead. For passwords and PINs, use `fill-password`.
30
+ If the possible answers are known in advance, list them in your question. For passwords and PINs, use `fill-password` instead.
31
31
 
32
32
  ## Browsers
33
33
 
@@ -3,15 +3,10 @@ export interface PendingRequestMeta {
3
3
  session_id?: string;
4
4
  session_name?: string;
5
5
  description?: string;
6
- input_questions?: string[];
7
- /** Render the input field(s) masked (password entry). */
8
- secret?: boolean;
9
6
  }
10
7
  export interface PendingRequest {
11
- type: "confirmation" | "input" | "choice";
8
+ type: "confirmation" | "password";
12
9
  resolve: (value: string[]) => void;
13
- /** Input descriptions (for 'input') or option labels (for 'choice'). */
14
- params?: string[];
15
10
  /** Display context for PWAs that connect while this request is already open. */
16
11
  meta?: PendingRequestMeta;
17
12
  }
@@ -19,7 +14,7 @@ export interface PendingRequest {
19
14
  * Keyed by taskId. Only one pending request per key at a time. `meta` is surfaced
20
15
  * via host.info so a freshly-connected PWA can render the modal without replaying events.
21
16
  */
22
- export declare function registerPending(key: string, type: PendingRequest["type"], params?: PendingRequest["params"], meta?: PendingRequestMeta): Promise<string[]>;
17
+ export declare function registerPending(key: string, type: PendingRequest["type"], meta?: PendingRequestMeta): Promise<string[]>;
23
18
  export declare function resolvePending(key: string, value: string[]): boolean;
24
19
  export declare function getPending(key: string): PendingRequest | undefined;
25
20
  export declare function removePending(key: string): void;
@@ -27,6 +22,5 @@ export declare function removePending(key: string): void;
27
22
  export declare function listPending(): Array<{
28
23
  key: string;
29
24
  type: PendingRequest["type"];
30
- params?: PendingRequest["params"];
31
25
  meta?: PendingRequestMeta;
32
26
  }>;
@@ -3,12 +3,12 @@ const pending = new Map();
3
3
  * Keyed by taskId. Only one pending request per key at a time. `meta` is surfaced
4
4
  * via host.info so a freshly-connected PWA can render the modal without replaying events.
5
5
  */
6
- export function registerPending(key, type, params, meta) {
6
+ export function registerPending(key, type, meta) {
7
7
  if (pending.has(key)) {
8
8
  return Promise.reject(new Error(`Key ${key} already has a pending request`));
9
9
  }
10
10
  return new Promise((resolve) => {
11
- pending.set(key, { type, resolve, params, meta });
11
+ pending.set(key, { type, resolve, meta });
12
12
  });
13
13
  }
14
14
  export function resolvePending(key, value) {
@@ -30,7 +30,6 @@ export function listPending() {
30
30
  return [...pending.entries()].map(([key, entry]) => ({
31
31
  key,
32
32
  type: entry.type,
33
- params: entry.params,
34
33
  meta: entry.meta,
35
34
  }));
36
35
  }