@pi-harness/pi-harness 0.1.81 → 0.1.82

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.
@@ -1782,18 +1782,39 @@ export default {
1782
1782
  sendJson(response, 405, { error: "Method not allowed" });
1783
1783
  return;
1784
1784
  }
1785
- if (busy) {
1786
- sendJson(response, 409, { error: "Another prompt is already running" });
1787
- return;
1788
- }
1789
- busy = true;
1785
+ let ownsBusy = false;
1790
1786
  let unsubscribe: (() => void) | undefined;
1791
1787
  try {
1792
- const payload = JSON.parse(await bodyText(request)) as { prompt?: unknown };
1788
+ const payload = JSON.parse(await bodyText(request)) as { prompt?: unknown; streamingBehavior?: unknown };
1793
1789
  if (typeof payload.prompt !== "string" || payload.prompt.trim().length === 0) {
1794
1790
  sendJson(response, 400, { error: "Prompt must be a non-empty string" });
1795
1791
  return;
1796
1792
  }
1793
+ if (payload.streamingBehavior !== undefined && payload.streamingBehavior !== "steer" && payload.streamingBehavior !== "followUp") {
1794
+ sendJson(response, 400, { error: 'streamingBehavior must be "steer" or "followUp"' });
1795
+ return;
1796
+ }
1797
+ const streamingBehavior = payload.streamingBehavior;
1798
+ if (services.runtime.session.isStreaming) {
1799
+ if (!streamingBehavior) {
1800
+ sendJson(response, 409, { error: "Another prompt is already running; choose steer or followUp delivery" });
1801
+ return;
1802
+ }
1803
+ await services.runtime.prompt(payload.prompt, { streamingBehavior });
1804
+ sendJson(response, 200, {
1805
+ reply: "",
1806
+ messages: services.runtime.session.messages.length,
1807
+ queued: true,
1808
+ streamingBehavior,
1809
+ });
1810
+ return;
1811
+ }
1812
+ if (busy) {
1813
+ sendJson(response, 409, { error: "Another prompt is already running" });
1814
+ return;
1815
+ }
1816
+ busy = true;
1817
+ ownsBusy = true;
1797
1818
  const chunks: string[] = [];
1798
1819
  unsubscribe = services.runtime.session.subscribe((event) => {
1799
1820
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") chunks.push(event.assistantMessageEvent.delta);
@@ -1813,7 +1834,7 @@ export default {
1813
1834
  sendJson(response, 400, { error: errorText(error) });
1814
1835
  } finally {
1815
1836
  unsubscribe?.();
1816
- busy = false;
1837
+ if (ownsBusy) busy = false;
1817
1838
  }
1818
1839
  },
1819
1840
  });
@@ -1688,16 +1688,27 @@ describe("API gateway plugin", () => {
1688
1688
  });
1689
1689
  });
1690
1690
 
1691
- test("serializes concurrent prompts and validates input", async () => {
1691
+ test("serializes ordinary prompts, accepts steering while streaming, and validates input", async () => {
1692
1692
  const context = new Context();
1693
1693
  contexts.push(context);
1694
1694
  await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
1695
- const session = { messages: [], subscribe: () => () => {} };
1695
+ const session = {
1696
+ messages: [],
1697
+ subscribe: () => () => {},
1698
+ get isStreaming() {
1699
+ return promptStarted;
1700
+ },
1701
+ };
1696
1702
  let promptStarted = false;
1697
1703
  let releasePrompt: (() => void) | undefined;
1704
+ const queued: Array<{ text: string; streamingBehavior?: string }> = [];
1698
1705
  const runtime = {
1699
1706
  session,
1700
- prompt: () => {
1707
+ prompt: (text: string, options?: { streamingBehavior?: string }) => {
1708
+ if (options?.streamingBehavior) {
1709
+ queued.push({ text, streamingBehavior: options.streamingBehavior });
1710
+ return Promise.resolve();
1711
+ }
1701
1712
  promptStarted = true;
1702
1713
  return new Promise<void>((resolve) => {
1703
1714
  releasePrompt = resolve;
@@ -1722,6 +1733,14 @@ describe("API gateway plugin", () => {
1722
1733
  body: JSON.stringify({ prompt: "second" }),
1723
1734
  }),
1724
1735
  ).resolves.toMatchObject({ status: 409 });
1736
+ const steering = await fetch(context.webServer.url + "/api/prompt", {
1737
+ method: "POST",
1738
+ headers: { "content-type": "application/json" },
1739
+ body: JSON.stringify({ prompt: "correct course", streamingBehavior: "steer" }),
1740
+ });
1741
+ expect(steering.status).toBe(200);
1742
+ await expect(steering.json()).resolves.toMatchObject({ queued: true, streamingBehavior: "steer" });
1743
+ expect(queued).toEqual([{ text: "correct course", streamingBehavior: "steer" }]);
1725
1744
  releasePrompt?.();
1726
1745
  await expect(first).resolves.toMatchObject({ status: 200 });
1727
1746
  await expect(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/web-app",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "private": true,
5
5
  "description": "Patchable Web application bundle for Pi Harness",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/cli",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "private": true,
5
5
  "description": "Plugin-first Pi Harness CLI",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/host-webserver",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "private": true,
5
5
  "description": "Cordis route-registration service for the Pi Harness web host",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",