DEV Community

Evan Lin for Google Developer Experts

Posted on Originally published at evanlin.com on

[AI in Practice] Migrating a LINE Research Bot from Go ADK to DeepSeek Harness (DSH): Architecture, Mapping, and Pitfalls

Before / after architecture: Go ADK vs. DeepSeek Harness

Before (ADK for Go) After (DeepSeek Harness)
Where the agent runs Inside the same Go process One dsh subprocess per stage
How it reaches Gemini Direct connection to Vertex AI DSH (Messages protocol) → LiteLLM sidecar → Vertex AI
Structured output OutputSchema submit_* tools + reading session events
Google Search ADK built-in geminitool Custom web_research tool
Cloud Run 1 container 2 containers (app + LiteLLM), still scales to zero

Background

In my previous post, I rewrote my LINE research-planner bot from Python + Managed Agents to Go + ADK for Go, resulting in kkdai/line-research-bot-go.

The user experience is unchanged:

  • Send "Research how to choose a SOTA open-source vector database" on LINE, and the bot runs three stages in sequence, PLAN → SEARCH_COMPARE → WRITE_REPORT, then replies with a Flex card containing a summary, key sources, and a "Read full report" button.
  • Follow up with "Go deeper on Chapter 2 and add Japanese sources", and the same report is refined in place: the version number is bumped and the previous version is saved as a snapshot.

Around the same time, DeepSeek open-sourced deepseek-ai/dsh-superagent-prototype, a "super agent" prototype built on DeepSeek Harness (DSH). I was curious what this framework would look like behind a LINE bot, so I set myself a challenge:

Keep the LINE bot features and Cloud Run setup exactly as they are, and swap only the agent framework from ADK to the dsh-superagent stack.

The short answer: it works, and it is already running in production, but not in the way I originally expected. This post covers three things:

  1. What the DSH framework actually is (with an architecture diagram), and what dsh-superagent builds on top of it.
  2. How to migrate from ADK: concept mapping and real code.
  3. What goes wrong: the pitfalls I actually hit, collected into a checklist.
Repo Description
kkdai/line-research-bot-go Previous post: Go 1.27 + ADK for Go v1.9.0
kkdai/line-research-bot-dsh This post: TypeScript (Node 24) + DeepSeek Harness 0.2.1-rc.5. The LINE webhook currently points here.

1. Understanding DeepSeek Harness (DSH)

Many people assume dsh-superagent is an agent SDK. In fact, it consists of two separate layers:

Layer What it is On npm
DeepSeek Harness (DSH) An "agent runtime" that handles model calls, tools, sessions, permissions, and sandboxing, all implemented as plugins @deepseek-ai/dsh (CLI), plus 100+ @deepseek-ai/dsh-* packages
dsh-superagent A bundle (plugin pack) running on DSH that turns it into a proactive agent that works on a GitHub repository A Node package installed into a DSH profile via dsh plugin add

DSH's core design: everything is a Cordis plugin

DSH is built on Cordis (@deepseek-ai/cordis), a plugin framework. Models, tools, session storage, sandboxing, even the JSON-RPC server: every piece is a plugin (a "plugin row"), and plugins communicate through services exposed on ctx, such as ctx.llm, ctx.tools, ctx.fs, and ctx.subprocess.

How plugins are composed is determined by a Profile:

  • Bundle: an official, pre-assembled set of plugins, e.g. dsh-sdk-minimal, dsh-base, web, tui, headless.
  • Profile: lives under $DSH_HOME/profiles/<name>/ and specifies which bundles to load.
  • Patch layers: YAML patches layered on top of bundles. A patch can change a plugin's config, disable it with disabled: true, or insert your own plugins. Run dsh --profile x --dump-config to see the fully resolved tree.
# Example patch: tweak model settings, disable the shell, insert a custom plugin
- id: llm-deepseek
  config:
    thinking: disabled
- id: persistent-bash
  disabled: true
- insert:
    - id: line-research-tools
      name: 'file:///app/dsh/plugin/index.js'

Enter fullscreen mode Exit fullscreen mode

DSH architecture

DeepSeek Harness (DSH) architecture

A few facts that follow from this diagram are critical for the migration:

  1. The agent runs in a separate process. Unlike ADK's runner.Run(), which runs inside your program, with DSH you spawn a dsh runtime subprocess and talk to it over stdio JSON-RPC. The TypeScript client is @deepseek-ai/dsh-sdk-client; a Python SDK is also available.
  2. The model layer only speaks the Anthropic Messages protocol. The official adapter, llm-deepseek, targets https://api.deepseek.com/anthropic by default. You can change baseURL, but the other side must be a /v1/messages-compatible endpoint. There is no Gemini adapter.
  3. Tools are registered by plugins. You write a Cordis plugin and call ctx.tools.register(defineTool({...})) inside apply(ctx). DSH validates arguments against the parameter schema; on failure, the error is returned to the model so it can correct itself.
  4. A session is an event stream. Every run() returns the full list of session events: turn/start, tool/call, tool/result, assistant/message, turn/end, and so on. This is what you rely on later to extract structured results.

What dsh-superagent builds on DSH

dsh-superagent is a prototype of a "proactive coding agent" used internally by the DeepSeek team. Its README is explicit: it is a research prototype, not a production product. Its domain model looks like this:

dsh-superagent domain model

  • A Source converts GitHub notifications and Feishu messages into Triggers.
  • Eval decides whether to accept a job based on Policy (who can issue commands, where, and with what authority). Accepted work becomes a Ticket.
  • Each Ticket is handled by an Executor Session working in its own directory. Results flow back to the original conversation through a Channel, e.g. by opening a PR.
  • A web dashboard shows which items need a human decision.

The design is very thorough, and it exposes a SourceKind extension point (docs/008-cookbook.md), so in theory you could add a LINE Source.


2. Feasibility: why I did not simply fork dsh-superagent

My first instinct was to fork dsh-superagent, add a LINE Source, and deploy it to Cloud Run. After reading through the docs and code, though, it became clear that its assumptions are almost the exact opposite of Cloud Run's:

Aspect dsh-superagent Existing Cloud Run architecture
Execution model A single long-running process (scripts/dsh-superagent start, launched in tmux) Stateless, scales to zero
State SQLite + lock files under stateDir Firestore
Event source Actively polls GitHub notifications; Feishu long-lived connection LINE pushes via webhook
HTTP Management API listens only on 127.0.0.1, with same-origin checks /webhook must be publicly reachable
Required config A GitHub repo and an agent account are mandatory; otherwise the runtime stays in "awaiting configuration" Only a LINE channel
Agent permissions danger-full-access, runs shell commands as the OS user Only needs web access for research
Model Anthropic Messages (DeepSeek) Vertex AI Gemini

Forcing a fork would mean removing the mandatory GitHub configuration, building a public, signature-verified webhook, pinning Cloud Run to min = max = 1 instances, and mounting Filestore for SQLite. In other words: giving up scale-to-zero and exposing an alpha-stage process capable of running arbitrary shell commands directly to the internet.

So I weighed two options:

  • Option A: Fork dsh-superagent and add a LINE Source. Keeps Eval / Ticket / dashboard, but is the most expensive and the most fragile.
  • Option B: Keep the Cloud Run architecture and replace only the agent layer with DSH. ✅ Chosen. The LINE webhook, Cloud Tasks, Firestore, and GCS stay untouched; each of the three research stages runs its own DSH session.

For the model, I chose to keep using Gemini on Vertex AI, with LiteLLM in between as a protocol translator. The consequences of that decision are covered in Section 3.

💡 Takeaway: Before "replacing framework A with framework B", be clear about whether you are replacing the runtime (the harness) or the application built on top of it (the superagent). What I actually needed here was the former.


3. Migrating from ADK to DSH: architecture and mapping

Post-migration architecture

Post-migration architecture on Cloud Run

On Cloud Run this is one service with two containers: app (ingress) and a litellm sidecar, communicating over localhost. The service still scales to zero.

ADK → DSH mapping

Concept ADK for Go (previous post) DeepSeek Harness (this post)
Where the agent runs In-process, runner.Run() One dsh runtime subprocess per stage, driven over stdio JSON-RPC
Agent definition llmagent.New(...) × 3 One profile (sdk-minimal + patch) + persona; stage instructions go in the user message
Model gemini.NewModel, direct to Vertex AI DSH Messages adapter → LiteLLM sidecar → Vertex AI Gemini
Google Search / URL Context geminitool.GoogleSearch{} + url_context Custom plugin tool web_research (Gemini + googleSearch + urlContext)
Structured output OutputSchema (auto-injects set_model_response) Custom submit_plan / submit_sources / submit_report tools; arguments read back from session events
Stage handoff Session State {plan}, {sources}, {report_md?} Firestore workspace, injected directly into the stage prompt
Trace ID ADK InvocationID DSH sessionId (still stored in last_interaction_id)
Tool permissions Only the tools each agent needs Patch disables bash, editor, and working-directory; only research tools remain
Language Go 1.27 TypeScript (Node 24 runs .ts directly, no build step)

Everything on the LINE side — intent classification, Flex cards, Firestore document schema, GCS paths, the Cloud Tasks flow, deepen / retry — was ported 1:1 from Go, down to the exact message text. The real work in this migration comes down to four rewrites in the agent layer.

Rewrite 1: runner.Run() → DeepSeekHarness.run()

ADK runs the agent in-process; with DSH, each stage starts a runtime and shuts it down when done (src/research/agents.ts):

const harness = new DeepSeekHarness({
  profile: 'sdk-minimal',
  patches: [patch], // research.patch.yml, rendered at startup
  dshHome: this.opts.dshHome, // /tmp/dsh-home on Cloud Run
  cwd: workdir, // an empty temp directory per stage
  provider: 'deepseek-official', // routes through the llm-deepseek adapter
  model: this.opts.model, // gemini-3.8-flash, an alias LiteLLM recognizes
  maxTokens: 32_000,
  env: {
    ...runtimeEnv(process.env), // strips LINE secrets
    DEEPSEEK_BASE_URL: this.opts.llmBaseURL, // http://127.0.0.1:4000
    DEEPSEEK_API_KEY: this.opts.llmApiKey, // LiteLLM master key
    DSH_SYSTEM_PROMPT: PERSONA,
  },
})
try {
  const result = await harness.run(stagePrompt)
  assertTurnCompleted(result.events)
  return extractSubmission(result.events, 'submit_plan')
} finally {
  await harness.close()
}

Enter fullscreen mode Exit fullscreen mode

provider: 'deepseek-official' looks odd — why say DeepSeek when the model is Gemini? It is simply the route name of the DSH adapter, meaning "use the Messages protocol path". Where requests actually go is determined by DEEPSEEK_BASE_URL, which points to LiteLLM.

⚠️ The SDK has no mid-run cancellation. The way to enforce a timeout is to close() the runtime. I wired this up with an AbortSignal; each stage still has a 3 / 10 / 8-minute limit.

Rewrite 2: OutputSchema → submit_* tools

ADK's OutputSchema is convenient, and DSH has no equivalent. But under the hood, ADK simply auto-injects a set_model_response tool, so I did the same thing myself:

// dsh/plugin/index.js: a Cordis plugin
export const name = 'line-research-tools'
export const inject = ['tools']

export function apply(ctx) {
  ctx.tools.register(defineTool({
    name: 'submit_plan',
    description: 'Submit the research plan. Call exactly once when the plan is final.',
    parameters: {
      topic: { type: 'string', required: true },
      queries: { type: 'array', required: true, items: { type: 'object', additionalProperties: true,
        properties: { q: { type: 'string', required: true }, source_category: { type: 'string', required: true },
                      lang: { type: 'string', required: true } } } },
      source_count: { type: 'integer', required: true },
    },
    output: ack,
    async execute() { return { received: true } },
  }))
}

Enter fullscreen mode Exit fullscreen mode

DSH validates the arguments against the schema first; if validation fails, the error goes back to the model so it can fix the call and retry. On the host side, all you need is to find "the last submit_plan call that was not rejected" in the session events (src/research/submission.ts):

for (const e of events) {
  if (e.type === 'tool/call' && e.data.name === tool) calls.set(e.data.callId, e.data.arguments)
  else if (e.type === 'tool/result' && e.data.message.isError) failed.set(e.data.message.toolCallId, text)
}
const accepted = [...calls].filter(([id]) => !failed.has(id))

Enter fullscreen mode Exit fullscreen mode

There is a nice side benefit: the issue from the previous post where the model returned malformed enum values like chapter_not_found_chapter_not_found is now caught by DSH at the tool layer via enum validation, and the model is asked to resubmit.

Rewrite 3: a LiteLLM sidecar to connect DSH to Gemini

DSH only speaks Anthropic Messages; Gemini on Vertex AI does not. LiteLLM fits neatly in between: it exposes /v1/messages externally, calls vertex_ai/gemini-3.8-flash internally, and authenticates with the Cloud Run service account (ADC).

model_list:
  - model_name: gemini-3.8-flash
    litellm_params:
      model: vertex_ai/gemini-3.8-flash
      vertex_project: <your-project>
      vertex_location: global
litellm_settings:
  drop_params: true
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY

Enter fullscreen mode Exit fullscreen mode

The multi-container Cloud Run service is declared in service.yaml, with container-dependencies so the app waits for LiteLLM to come up first:

annotations:
  run.googleapis.com/container-dependencies: '{"app":["litellm"]}'
containers:
  - name: app # ingress, port 8080
  - name: litellm # sidecar, port 4000, startupProbe: /health/liveliness

Enter fullscreen mode Exit fullscreen mode

Rewrite 4: a web_research tool to preserve Google Search grounding

This was the part I cared about most. Much of the ADK version's quality came from Google Search + URL Context grounding. Once requests are translated to the Messages protocol by LiteLLM, these Gemini-specific tools are effectively lost.

The fix is to keep grounding out of the translation layer: a web_research tool calls Vertex AI directly via @google/genai, with googleSearch and urlContext enabled:

const res = await ai.models.generateContent({
  model: researchModel,
  contents: prompt,
  config: { tools: [{ googleSearch: {} }, { urlContext: {} }], abortSignal: exec.signal },
})
// groundingChunks URLs are vertexaisearch redirects; one HEAD request resolves the real URL

Enter fullscreen mode Exit fullscreen mode

The result: Gemini (via DSH) handles reasoning and planning, while Gemini (via the tool) handles web research. DSH lets the model call web_research in parallel (the tool is marked isConcurrencySafe), so the search stage turned out faster than I expected.


4. Pitfalls: what actually went wrong

Everything below is something I actually ran into during this migration. DSH is currently at 0.2.1-rc.5, and a lot of behavior is either undocumented or documented for an older version.

Pitfall 1: SDK options were renamed, and the old ones are silently ignored

The README for @deepseek-ai/dsh-sdk-client shows this example:

new DeepSeekHarness({ launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] }, ... })

Enter fullscreen mode Exit fullscreen mode

Copying it, everything appeared to work and the model responded. But when I asked it to list its tools, out came bash, subagent, create_goal, web_search… my patch had no effect at all. Digging into the SDK source revealed that rc.5 had already switched to profile / patches / dshHome / env: launch does not raise an error — it is simply ignored, and the SDK quietly falls back to the fully loaded sdk profile.

✅ How to verify: print tools[].name from the request/header event to see the exact tool list the model receives. After the fix, only read_image, web_research, submit_* remained.

Pitfall 2: plugin names are resolved relative to $DSH_HOME/profiles/<profile>/

The PoC worked fine, but the real integration test failed:

AgentError: every submit_plan call was rejected: Error: unknown tool "submit_plan"

Enter fullscreen mode Exit fullscreen mode

Launching the runtime directly and reading stderr revealed the cause:

line-research-tools: failed to import: Cannot find package 'line-research-tools'
imported from /tmp/.../profiles/sdk-minimal/

Enter fullscreen mode Exit fullscreen mode

DSH resolves plugins by walking up from the profile directory looking for node_modules. In the PoC, DSH_HOME happened to live inside the project directory, so resolution worked by coincidence. On Cloud Run, DSH_HOME=/tmp/dsh-home, so it is guaranteed to fail.

Next I tried supplying the path dynamically with name: !!js process.env.XXX, which was also rejected:

disabling profile plugin row "line-research-tools": its declared dsh requirements cannot be validated: name.startsWith is not a function

Enter fullscreen mode Exit fullscreen mode

name is checked before !!js is evaluated, so it must be a literal string. The final solution: at startup, render the patch template into DSH_HOME with the plugin's absolute file:// URL hard-coded.

💡 This is the most important lesson of the whole migration: a single directory difference between PoC and production completely changed the outcome. Integration tests must use the same DSH_HOME location as production.

Pitfall 3: changing apiKeyEnv in a patch has no effect

I changed apiKeyEnv for llm-deepseek to LITELLM_API_KEY in a patch, and --dump-config confirmed the change, yet at runtime I still got:

MISSING_CREDENTIAL: no API key for provider route "deepseek-official"; ... export DEEPSEEK_API_KEY

Enter fullscreen mode Exit fullscreen mode

In the end, simply passing LiteLLM's key and URL through the DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL environment variables worked. Also note that a patch's config replaces the block entirely rather than merging, so you need to restore defaults such as defaultContextWindow and streamIdleTimeoutMs yourself.

Pitfall 4: different profiles cannot share the same sessions directory

session artifact ".../session.v4.jsonl.zstd" uses .jsonl.zstd, but this backend is configured for compression "none"

Enter fullscreen mode Exit fullscreen mode

The sdk profile compresses sessions (zstd); sdk-minimal does not. If two different profiles run against the same DSH_HOME, they end up reading each other's files. Production only uses one profile, so this is not an issue there, but remember to clear $DSH_HOME/sessions when experimenting locally.

Pitfall 5: Node's type stripping does not support constructor parameter properties

Node 24 can run node src/main.ts directly with no build step, which is great. But it only strips erasable syntax, so code like this fails outright:

constructor(private readonly store: Store) {} // ❌ TS1294

Enter fullscreen mode Exit fullscreen mode

Enable erasableSyntaxOnly: true in tsconfig.json so tsc catches this early. Also, .ts files under node_modules are not type-stripped, so DSH plugins must be written in plain JavaScript.

Pitfall 6: npx node@24 does not actually run Node 24

I wanted to confirm the container's Node 24 could run everything, but npx node@24 --version printed v26.0.0: npx still used the node on PATH. I ended up downloading the Node 24 tarball directly from nodejs.org (verify it against SHASUMS256) to run the tests, which also confirmed the DSH runtime starts cleanly on Node 24.

Pitfall 7: LiteLLM needs extra Vertex AI dependencies

After pip install 'litellm[proxy]', the very first request failed with:

litellm.APIConnectionError: No module named 'google'

Enter fullscreen mode Exit fullscreen mode

The Vertex AI path requires google-auth + google-cloud-aiplatform, which you must add to the image yourself and pin (I use litellm 1.106.6, google-auth 2.62.1, google-cloud-aiplatform 2.7.0). In addition, recent LiteLLM versions refuse to start without a master key, and requests without a key return 500 rather than 401 (due to missing prisma), although they are still rejected. Fortunately the sidecar only listens on localhost inside the instance and is not reachable externally.

Pitfall 8: the agent subprocess does not need your secrets

By default the DSH runtime inherits the entire process.env, which means the LINE Channel Secret and Access Token end up inside the agent's process. Even though the shell tools are disabled, I wrote a runtimeEnv() helper to strip them out, with unit tests. This matches what the dsh-superagent docs say: wrappers and directories are defaults, not security boundaries — don't hand over anything you don't have to.

Pitfall 9: it is slower than the ADK version

The integration test (plan → search → write → deepen with a missing chapter) took about 7.5 minutes, versus roughly 150–190 seconds for the ADK version. Contributing factors: an extra runtime startup per stage (about 1–2 seconds), the additional LiteLLM hop, and many web_research calls. Everything still fits within Cloud Tasks' 30-minute dispatch deadline, but I relaxed the stage timeouts from ADK's 3 / 8 / 6 minutes to 3 / 10 / 8 minutes.

Pitfall 10: pin pre-release dependencies

DSH publishes three release lines on npm simultaneously: latest (0.0.1-rc.1 or 0.1.x), next (0.2.1-rc.5), and alpha (0.2.2-alpha.3), and latest points to different versions for different packages. That is exactly how the SDK option rename in Pitfall 1 bit me. I pinned everything to the next line: @deepseek-ai/dsh, dsh-sdk-client, and dsh-tools are all at 0.2.1-rc.5.


5. Deployment and cutover: reusing the existing service's configuration

Deployment differs from the previous post: this time I created a new Cloud Run service, since the sidecar changes the container layout. All configuration, however, was carried over from the original service:

  1. describe the original service first to get the service account, bucket, queue, and Secret Manager bindings for the LINE secrets.
  2. Reuse everything in the new service: same SA, same bucket, same queue, same LINE secrets, and the same Firestore collections. That way, after the cutover, users can keep "deepening" their existing reports. The only new secret is LITELLM_MASTER_KEY.
  3. Build two images with Cloud Build (my laptop still has no Docker) and deploy with gcloud run services replace service.yaml. CLOUD_RUN_SERVICE_URL is precomputed using Cloud Run's deterministic https://<service>-<project-number>.<region>.run.app format, so there is no need to deploy twice.
  4. Smoke tests: /readyz returns 200, an invalid signature returns 401, a missing OIDC token returns 401, a request signed with the real Channel Secret returns 200, and both containers pass their startup probes.
  5. Switch the webhook via the LINE API — again, no console required:
TOKEN=$(gcloud secrets versions access latest --secret=LINE_CHANNEL_ACCESS_TOKEN)
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"endpoint":"<NEW_SERVICE_URL>/webhook"}' \
  https://api.line.me/v2/bot/channel/webhook/endpoint
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' \
  https://api.line.me/v2/bot/channel/webhook/test

Enter fullscreen mode Exit fullscreen mode
  1. Confirm traffic in the logs: after the switch, the new service received LineBotWebhook/2.0 requests and the old service dropped to zero. The first real user's research job then ran on the new service from job start to job done in about 6 minutes 43 seconds with no errors. All Gemini calls through the LiteLLM sidecar returned 200, and a ~29 KB index.html report appeared in GCS.

Rolling back is just a matter of pointing the webhook back at the original service. Because both services share Firestore, switching back and forth loses no data.


6. ADK → DSH migration checklist

  • 1. Decide whether you are replacing the harness or the app on top of it: dsh-superagent is a GitHub coding agent; a chat bot on Cloud Run needs DSH itself.
  • 2. Build a PoC first, using the production directory layout: keep DSH_HOME outside the project (e.g. /tmp) so plugin resolution issues surface early.
  • 3. Print the tool list the model actually receives (request/header event) to confirm the patch is applied and dangerous tools are really disabled.
  • 4. Follow the type definitions for SDK options, not the README: options get renamed between RCs, and old ones fail silently.
  • 5. Use a literal absolute file:// URL for the plugin name; !!js does not work here.
  • 6. OutputSchema → submit_* tools: let DSH validate the schema, and have the host pick the last accepted call from tool/call / tool/result events.
  • 7. Use a Messages-compatible gateway (LiteLLM) for non-DeepSeek models, pass credentials via DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL, and remember that patch configs replace rather than merge.
  • 8. Put vendor-specific capabilities (Google Search grounding) inside tools; don't expect the translation layer to preserve them.
  • 9. Filter environment variables for the agent subprocess; never hand it your Channel Secret.
  • 10. Pin all pre-release packages to the same dist-tag line, and actually run the container's Node version at least once.
  • 11. Reuse the old service's SA / bucket / queue / secrets / Firestore in the new service, switch the webhook via the LINE API, verify traffic in the logs, and roll back by reverting the webhook.
  • 12. Never expose real .run.app URLs in public repos, issues, or posts.

Summary and open-source links

In this migration I replaced the agent layer of kkdai/line-research-bot-go — ADK for Go — with DeepSeek Harness, producing kkdai/line-research-bot-dsh. The key takeaways:

  1. DSH is a composable agent runtime. The Cordis plugin + profile layering design is very clean: disabling, swapping, or inserting any component takes just a few lines of YAML. The trade-off is that the agent runs in a separate process and must be driven over JSON-RPC.
  2. dsh-superagent is an excellent reference design for proactive agents (Source / Eval / Ticket / Mandate), but its execution model — a long-running process plus SQLite — is a poor fit for Cloud Run. Borrow its harness; don't force-port its app.
  3. Most of what ADK offers can be rebuilt on DSH as tools: OutputSchema becomes submit_*, and Google Search grounding becomes web_research. Schema validation at the tool layer is arguably more reliable than prompt-level constraints.
  4. The biggest risk with pre-release frameworks is not missing features but silent failures: ignored options, and plugin load failures that surface as "unknown tool". Every layer needs a way to be observed and verified directly.

The full source code is at kkdai/line-research-bot-dsh, and the design trade-offs are documented in the repo's docs/design.md. Feedback and discussion are welcome!

Further reading

Top comments (0)