diff --git a/adapters/pi/adapter.sh b/adapters/pi/adapter.sh index 9af212a9..2515b38b 100644 --- a/adapters/pi/adapter.sh +++ b/adapters/pi/adapter.sh @@ -18,6 +18,18 @@ if [ -n "${MOSAIC_WORKSPACE:-}" ]; then cd "$MOSAIC_WORKSPACE" fi +# Session (M6): persistent named session directory; resume the most recent +# session in that directory when one exists (pi documented flags). +# Default remains ephemeral (--no-session) when no session is declared. +SESSION_FLAGS="--no-session" +if [ -n "${MOSAIC_SESSION_DIR:-}" ]; then + mkdir -p "$MOSAIC_SESSION_DIR" + SESSION_FLAGS="--session-dir $MOSAIC_SESSION_DIR" + if [ -n "$(ls -A "$MOSAIC_SESSION_DIR" 2>/dev/null)" ]; then + SESSION_FLAGS="$SESSION_FLAGS -c" + fi +fi + # Capabilities (M5): explicit allowlist or no tools. TOOLS_FLAG="--no-tools" [ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS" @@ -30,13 +42,13 @@ TOOLS_FLAG="--no-tools" # --offline no startup network operations (update checks/telemetry) exec pi \ --offline \ - --no-session \ --no-extensions \ --no-skills \ --no-prompt-templates \ --no-themes \ --no-context-files \ $TOOLS_FLAG \ + $SESSION_FLAGS \ --provider "$PI_PROVIDER" \ --model "$PI_MODEL" \ --system-prompt "$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" \ diff --git a/compose.yaml b/compose.yaml index 292d19cd..e10c2f8f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -18,6 +18,8 @@ services: # Workspace + capabilities (set by the task runner; M5) MOSAIC_WORKSPACE: ${MOSAIC_WORKSPACE:-} MOSAIC_TOOLS: ${MOSAIC_TOOLS:-} + # Persistent named session dir (set by the task runner; M6) + MOSAIC_SESSION_DIR: ${MOSAIC_SESSION_DIR:-} # mock adapter only: verbatim response for deterministic seam tests MOSAIC_MOCK_RESPONSE: ${MOSAIC_MOCK_RESPONSE:-} # Documented container auth alternative: provider API key via diff --git a/scripts/mosaic-task.mjs b/scripts/mosaic-task.mjs index 77c4c785..de852799 100755 --- a/scripts/mosaic-task.mjs +++ b/scripts/mosaic-task.mjs @@ -106,7 +106,7 @@ function validateMission(document, file) { function validateTask(document, file) { if (!isPlainObject(document)) fail(2, "task must be a JSON object"); - rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds", "workspace", "capabilities"], "task"); + rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds", "workspace", "capabilities", "session"], "task"); if (document.taskVersion !== 1) { fail(2, `unsupported taskVersion: ${JSON.stringify(document.taskVersion)} (supported: 1)`); } @@ -158,6 +158,17 @@ function validateTask(document, file) { workspace = document.workspace; } + // Session (M6): optional named persistent session under + // /sessions/. Distinct names never share state. + let session = null; + if (document.session !== undefined && document.session !== null) { + if (typeof document.session !== "string" || document.session.length === 0) { + fail(2, 'task "session" must be a non-empty string when present'); + } + validateId(document.session, "task session"); + session = document.session; + } + // Capabilities (M5): optional tools allowlist mapped by adapters to their // native permission flags. Absent = no tools. let tools = null; @@ -189,6 +200,7 @@ function validateTask(document, file) { timeoutSeconds, workspace, tools, + session, }; } @@ -271,6 +283,12 @@ function runTask(taskFile) { if (workspaceContainerPath) spawnEnv.MOSAIC_WORKSPACE = workspaceContainerPath; spawnEnv.MOSAIC_TOOLS = task.tools ? task.tools.join(",") : ""; + // Session (M6): persistent named session dir, passed as container path. + if (task.session) { + fs.mkdirSync(path.join(resolved.dataRoot, "sessions", task.session), { recursive: true }); + spawnEnv.MOSAIC_SESSION_DIR = `/var/lib/mosaic/sessions/${task.session}`; + } + const proc = spawnSync( "docker", ["compose", "run", "--rm", "-T", "mosaic-agent", task.prompt], @@ -319,6 +337,7 @@ function runTask(taskFile) { expectedExact: expected, workspace: task.workspace, tools: task.tools, + session: task.session, exitCode: proc.status, signal: proc.signal ?? null, provider: resolved.execution.provider, diff --git a/tasks/session-demo-1.json b/tasks/session-demo-1.json new file mode 100644 index 00000000..1aaa9174 --- /dev/null +++ b/tasks/session-demo-1.json @@ -0,0 +1,8 @@ +{ + "taskVersion": 1, + "id": "t-session-teach", + "prompt": "Remember this code word for later: mosaico. Reply with exactly: REMEMBERED", + "session": "demo", + "expectExact": "REMEMBERED", + "timeoutSeconds": 180 +} diff --git a/tasks/session-demo-2.json b/tasks/session-demo-2.json new file mode 100644 index 00000000..228ccbc1 --- /dev/null +++ b/tasks/session-demo-2.json @@ -0,0 +1,7 @@ +{ + "taskVersion": 1, + "id": "t-session-recall", + "prompt": "What code word did I ask you to remember earlier in this session? Reply with only the code word.", + "session": "demo", + "timeoutSeconds": 180 +}